aboutsummaryrefslogtreecommitdiffstats
path: root/djangorestframework/resources.py
blob: adf5c1c3d0961dbacbf700594f3297bd5b62489e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
from django import forms
from django.core.urlresolvers import reverse, get_urlconf, get_resolver, NoReverseMatch
from django.db import models
from django.db.models.query import QuerySet
from django.db.models.fields.related import RelatedField
from django.utils.encoding import smart_unicode

from djangorestframework.response import ErrorResponse
from djangorestframework.utils import as_tuple

import decimal
import inspect
import re


# TODO: _IgnoreFieldException

# Map model classes to resource classes
#_model_to_resource = {}


def _model_to_dict(instance, resource=None):
    """
    Given a model instance, return a ``dict`` representing the model.
    
    The implementation is similar to Django's ``django.forms.model_to_dict``, except:

    * It doesn't coerce related objects into primary keys.
    * It doesn't drop ``editable=False`` fields.
    * It also supports attribute or method fields on the instance or resource.
    """
    opts = instance._meta
    data = {}

    #print [rel.name for rel in opts.get_all_related_objects()]
    #related = [rel.get_accessor_name() for rel in opts.get_all_related_objects()]
    #print [getattr(instance, rel) for rel in related]
    #if resource.fields:
    #    fields = resource.fields
    #else:
    #    fields = set(opts.fields + opts.many_to_many)
    
    fields = resource and resource.fields or ()
    include = resource and resource.include or ()
    exclude = resource and resource.exclude or ()

    extra_fields = fields and list(resource.fields) or []

    # Model fields
    for f in opts.fields + opts.many_to_many:
        if fields and not f.name in fields:
            continue
        if exclude and f.name in exclude:
            continue
        if isinstance(f, models.ForeignKey):
            data[f.name] = getattr(instance, f.name)
        else:
            data[f.name] = f.value_from_object(instance)
        
        if extra_fields and f.name in extra_fields:
            extra_fields.remove(f.name)
    
    # Method fields
    for fname in extra_fields:
        if hasattr(resource, fname):
            # check the resource first, to allow it to override fields
            obj = getattr(resource, fname)
            # if it's a method like foo(self, instance), then call it 
            if inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) == 2:
                obj = obj(instance)
        elif hasattr(instance, fname):
            # now check the object instance
            obj = getattr(instance, fname)
        else:
            continue

        # TODO: It would be nicer if this didn't recurse here.
        # Let's keep _model_to_dict flat, and _object_to_data recursive.
        data[fname] = _object_to_data(obj)
   
    return data


def _object_to_data(obj, resource=None):
    """
    Convert an object into a serializable representation.
    """
    if isinstance(obj, dict):
        # dictionaries
        # TODO: apply same _model_to_dict logic fields/exclude here
        return dict([ (key, _object_to_data(val)) for key, val in obj.iteritems() ])
    if isinstance(obj, (tuple, list, set, QuerySet)):
        # basic iterables
        return [_object_to_data(item, resource) for item in obj]
    if isinstance(obj, models.Manager):
        # Manager objects
        return [_object_to_data(item, resource) for item in obj.all()]
    if isinstance(obj, models.Model):
        # Model instances
        return _object_to_data(_model_to_dict(obj, resource))
    if isinstance(obj, decimal.Decimal):
        # Decimals (force to string representation)
        return str(obj)
    if inspect.isfunction(obj) and not inspect.getargspec(obj)[0]:
        # function with no args
        return _object_to_data(obj(), resource)
    if inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1:
        # bound method
        return _object_to_data(obj(), resource)

    return smart_unicode(obj, strings_only=True)


class BaseResource(object):
    """
    Base class for all Resource classes, which simply defines the interface they provide.
    """
    fields = None
    include = None
    exclude = None

    def __init__(self, view):
        self.view = view

    def validate_request(self, data, files=None):
        """
        Given the request content return the cleaned, validated content.
        Typically raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
        """
        return data
    
    def filter_response(self, obj):
        """
        Given the response content, filter it into a serializable object.
        """
        return _object_to_data(obj, self)


class Resource(BaseResource):
    """
    A Resource determines how a python object maps to some serializable data.
    Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets.
    """
    
    # The model attribute refers to the Django Model which this Resource maps to.
    # (The Model's class, rather than an instance of the Model)
    model = None
    
    # By default the set of returned fields will be the set of:
    #
    # 0. All the fields on the model, excluding 'id'.
    # 1. All the properties on the model.
    # 2. The absolute_url of the model, if a get_absolute_url method exists for the model.
    #
    # If you wish to override this behaviour,
    # you should explicitly set the fields attribute on your class.
    fields = None


class FormResource(Resource):
    """
    Resource class that uses forms for validation.
    Also provides a :meth:`get_bound_form` method which may be used by some renderers.

    On calling :meth:`validate_request` this validator may set a :attr:`bound_form_instance` attribute on the
    view, which may be used by some renderers.
    """

    """
    The :class:`Form` class that should be used for request validation.
    This can be overridden by a :attr:`form` attribute on the :class:`views.View`.
    """
    form = None


    def validate_request(self, data, files=None):
        """
        Given some content as input return some cleaned, validated content.
        Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
        
        Validation is standard form validation, with an additional constraint that *no extra unknown fields* may be supplied.

        On failure the :exc:`response.ErrorResponse` content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
        If the :obj:`'errors'` key exists it is a list of strings of non-field errors.
        If the :obj:`'field-errors'` key exists it is a dict of ``{'field name as string': ['errors as strings', ...]}``.
        """
        return self._validate(data, files)


    def _validate(self, data, files, allowed_extra_fields=(), fake_data=None):
        """
        Wrapped by validate to hide the extra flags that are used in the implementation.

        allowed_extra_fields is a list of fields which are not defined by the form, but which we still
        expect to see on the input.
        
        fake_data is a string that should be used as an extra key, as a kludge to force .errors
        to be populated when an empty dict is supplied in `data`
        """
        
        # We'd like nice error messages even if no content is supplied.
        # Typically if an empty dict is given to a form Django will
        # return .is_valid() == False, but .errors == {}
        #
        # To get around this case we revalidate with some fake data. 
        if fake_data:
            data[fake_data] = '_fake_data'
            allowed_extra_fields = allowed_extra_fields + ('_fake_data',)
        
        bound_form = self.get_bound_form(data, files)

        if bound_form is None:
            return data
        
        self.view.bound_form_instance = bound_form
        
        data = data and data or {}
        files = files and files or {}

        seen_fields_set = set(data.keys())
        form_fields_set = set(bound_form.fields.keys())
        allowed_extra_fields_set = set(allowed_extra_fields)

        # In addition to regular validation we also ensure no additional fields are being passed in...
        unknown_fields = seen_fields_set - (form_fields_set | allowed_extra_fields_set)
        unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept'))  # TODO: Ugh.

        # Check using both regular validation, and our stricter no additional fields rule
        if bound_form.is_valid() and not unknown_fields:
            # Validation succeeded...
            cleaned_data = bound_form.cleaned_data

            cleaned_data.update(bound_form.files)

            # Add in any extra fields to the cleaned content...
            for key in (allowed_extra_fields_set & seen_fields_set) - set(cleaned_data.keys()):
                cleaned_data[key] = data[key]

            return cleaned_data

        # Validation failed...
        detail = {}

        if not bound_form.errors and not unknown_fields:
            # is_valid() was False, but errors was empty.
            # If we havn't already done so attempt revalidation with some fake data
            # to force django to give us an errors dict.
            if fake_data is None:
                return self._validate(data, files, allowed_extra_fields, '_fake_data')

            # If we've already set fake_dict and we're still here, fallback gracefully.
            detail = {u'errors': [u'No content was supplied.']}

        else:       
            # Add any non-field errors
            if bound_form.non_field_errors():
                detail[u'errors'] = bound_form.non_field_errors()

            # Add standard field errors
            field_errors = dict(
                (key, map(unicode, val))
                for (key, val)
                in bound_form.errors.iteritems()
                if not key.startswith('__')
            )

            # Add any unknown field errors
            for key in unknown_fields:
                field_errors[key] = [u'This field does not exist.']
       
            if field_errors:
                detail[u'field-errors'] = field_errors

        # Return HTTP 400 response (BAD REQUEST)
        raise ErrorResponse(400, detail)
  

    def get_bound_form(self, data=None, files=None, method=None):
        """
        Given some content return a Django form bound to that content.
        If form validation is turned off (:attr:`form` class attribute is :const:`None`) then returns :const:`None`.
        """

        # A form on the view overrides a form on the resource.
        form = getattr(self.view, 'form', self.form)

        # Use the requested method or determine the request method
        if method is None and hasattr(self.view, 'request') and hasattr(self.view, 'method'):
            method = self.view.method
        elif method is None and hasattr(self.view, 'request'):
            method = self.view.request.method

        # A method form on the view or resource overrides the general case.
        # Method forms are attributes like `get_form` `post_form` `put_form`.
        if method:
            form = getattr(self, '%s_form' % method.lower(), form)
            form = getattr(self.view, '%s_form' % method.lower(), form)

        if not form:
            return None

        if data is not None:
            return form(data, files)

        return form()



#class _RegisterModelResource(type):
#    """
#    Auto register new ModelResource classes into ``_model_to_resource``
#    """
#    def __new__(cls, name, bases, dct):
#        resource_cls = type.__new__(cls, name, bases, dct)
#        model_cls = dct.get('model', None)
#        if model_cls:
#            _model_to_resource[model_cls] = resource_cls
#        return resource_cls



class ModelResource(FormResource):
    """
    Resource class that uses forms for validation and otherwise falls back to a model form if no form is set.
    Also provides a :meth:`get_bound_form` method which may be used by some renderers.
    """

    # Auto-register new ModelResource classes into _model_to_resource 
    #__metaclass__ = _RegisterModelResource

    """
    The form class that should be used for request validation.
    If set to :const:`None` then the default model form validation will be used.

    This can be overridden by a :attr:`form` attribute on the :class:`views.View`.
    """
    form = None

    """
    The model class which this resource maps to.

    This can be overridden by a :attr:`model` attribute on the :class:`views.View`.
    """
    model = None

    """
    The list of fields to use on the output.
    
    May be any of:
    
    The name of a model field.
    The name of an attribute on the model.
    The name of an attribute on the resource.
    The name of a method on the model, with a signature like ``func(self)``.
    The name of a method on the resource, with a signature like ``func(self, instance)``.
    """
    fields = None
    
    """
    The list of fields to exclude.  This is only used if :attr:`fields` is not set.
    """
    exclude = ('id', 'pk')
    
    """
    The list of extra fields to include.  This is only used if :attr:`fields` is not set.
    """
    include = ('url',)


    def __init__(self, view):
        """
        Allow :attr:`form` and :attr:`model` attributes set on the
        :class:`View` to override the :attr:`form` and :attr:`model`
        attributes set on the :class:`Resource`.
        """
        super(ModelResource, self).__init__(view)

        if getattr(view, 'model', None):
            self.model = view.model

    def validate_request(self, data, files=None):
        """
        Given some content as input return some cleaned, validated content.
        Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
        
        Validation is standard form or model form validation,
        with an additional constraint that no extra unknown fields may be supplied,
        and that all fields specified by the fields class attribute must be supplied,
        even if they are not validated by the form/model form.

        On failure the ErrorResponse content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
        If the :obj:`'errors'` key exists it is a list of strings of non-field errors.
        If the ''field-errors'` key exists it is a dict of {field name as string: list of errors as strings}.
        """
        return self._validate(data, files, allowed_extra_fields=self._property_fields_set)


    def get_bound_form(self, data=None, files=None, method=None):
        """
        Given some content return a ``Form`` instance bound to that content.

        If the :attr:`form` class attribute has been explicitly set then that class will be used
        to create the Form, otherwise the model will be used to create a ModelForm.
        """

        form = super(ModelResource, self).get_bound_form(data, files, method=method)
        
        # Use an explict Form if it exists
        if form:
            return form

        elif self.model:
            # Fall back to ModelForm which we create on the fly
            class OnTheFlyModelForm(forms.ModelForm):
                class Meta:
                    model = self.model
                    #fields = tuple(self._model_fields_set)

            # Instantiate the ModelForm as appropriate
            if data and isinstance(data, models.Model):
                # Bound to an existing model instance
                return OnTheFlyModelForm(instance=content)
            elif data is not None:
                return OnTheFlyModelForm(data, files)
            return OnTheFlyModelForm()

        # Both form and model not set?  Okay bruv, whatevs...
        return None
    

    def url(self, instance):
        """
        Attempts to reverse resolve the url of the given model *instance* for this resource.

        Requires a ``View`` with :class:`mixins.InstanceMixin` to have been created for this resource.
        
        This method can be overridden if you need to set the resource url reversing explicitly.
        """

        # dis does teh magicks...
        urlconf = get_urlconf()
        resolver = get_resolver(urlconf)

        possibilities = resolver.reverse_dict.getlist(self.view_callable[0])
        for tuple_item in possibilities:
            possibility = tuple_item[0]
            # pattern = tuple_item[1]
            # Note: defaults = tuple_item[2] for django >= 1.3
            for result, params in possibility:

                #instance_attrs = dict([ (param, getattr(instance, param)) for param in params if hasattr(instance, param) ])

                instance_attrs = {}
                for param in params:
                    if not hasattr(instance, param):
                        continue
                    attr = getattr(instance, param)
                    if isinstance(attr, models.Model):
                        instance_attrs[param] = attr.pk
                    else:
                        instance_attrs[param] = attr    

                try:
                    return reverse(self.view_callable[0], kwargs=instance_attrs)
                except NoReverseMatch:
                    pass
        raise NoReverseMatch


    @property
    def _model_fields_set(self):
        """
        Return a set containing the names of validated fields on the model.
        """
        model_fields = set(field.name for field in self.model._meta.fields)

        if fields:
            return model_fields & set(as_tuple(self.fields))

        return model_fields - set(as_tuple(self.exclude))
    
    @property
    def _property_fields_set(self):
        """
        Returns a set containing the names of validated properties on the model.
        """
        property_fields = set(attr for attr in dir(self.model) if
                              isinstance(getattr(self.model, attr, None), property)
                              and not attr.startswith('_'))

        if self.fields:
            return property_fields & set(as_tuple(self.fields))

        return property_fields.union(set(as_tuple(self.include))) - set(as_tuple(self.exclude))