aboutsummaryrefslogtreecommitdiffstats
path: root/djangorestframework/resources.py
diff options
context:
space:
mode:
authorTom Christie2011-05-13 09:59:36 +0100
committerTom Christie2011-05-13 09:59:36 +0100
commit8f6bcac7f3f156831343cc7fec79f624dcc2639f (patch)
treeea5017ba47fdbd4410e768c5851a098eed0045d5 /djangorestframework/resources.py
parent44c8b89c6051483677e72a6fc657b1e0457182d1 (diff)
downloaddjango-rest-framework-8f6bcac7f3f156831343cc7fec79f624dcc2639f.tar.bz2
cleanup
Diffstat (limited to 'djangorestframework/resources.py')
-rw-r--r--djangorestframework/resources.py568
1 files changed, 568 insertions, 0 deletions
diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py
new file mode 100644
index 00000000..f47b41d0
--- /dev/null
+++ b/djangorestframework/resources.py
@@ -0,0 +1,568 @@
+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
+
+import decimal
+import inspect
+import re
+
+
+
+def _model_to_dict(instance, fields=None, exclude=None):
+ """
+ This is a clone of Django's ``django.forms.model_to_dict`` except that it
+ doesn't coerce related objects into primary keys.
+ """
+ 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]
+
+ for f in opts.fields + opts.many_to_many:
+ #if not f.editable:
+ # continue
+ 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)
+
+ #print fields - (opts.fields + opts.many_to_many)
+ #for related in [rel.get_accessor_name() for rel in opts.get_all_related_objects()]:
+ # if fields and not related in fields:
+ # continue
+ # if exclude and related in exclude:
+ # continue
+ # data[related] = getattr(instance, related)
+
+ return data
+
+
+def _object_to_data(obj):
+ """
+ Convert an object into a serializable representation.
+ """
+ if isinstance(obj, dict):
+ # dictionaries
+ 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) for item in obj]
+ if isinstance(obj, models.Manager):
+ # Manager objects
+ return [_object_to_data(item) for item in obj.all()]
+ if isinstance(obj, models.Model):
+ # Model instances
+ return _object_to_data(_model_to_dict(obj))
+ 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())
+ if inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) == 1:
+ # method with only a 'self' args
+ return _object_to_data(obj())
+
+ # fallback
+ return smart_unicode(obj, strings_only=True)
+
+
+def _form_to_data(form):
+ """
+ Returns a dict containing the data in a form instance.
+
+ This code is pretty much a clone of the ``Form.as_p()`` ``Form.as_ul``
+ and ``Form.as_table()`` methods, except that it returns data suitable
+ for arbitrary serialization, rather than rendering the result directly
+ into html.
+ """
+ ret = {}
+ for name, field in form.fields.items():
+ if not form.is_bound:
+ data = form.initial.get(name, field.initial)
+ if callable(data):
+ data = data()
+ else:
+ if isinstance(field, FileField) and form.data is None:
+ data = form.initial.get(name, field.initial)
+ else:
+ data = field.widget.value_from_datadict(form.data, form.files, name)
+ ret[name] = field.prepare_value(data)
+ return ret
+
+
+class BaseResource(object):
+ """Base class for all Resource classes, which simply defines the interface they provide."""
+
+ def __init__(self, view):
+ self.view = view
+
+ def validate(self, data, files):
+ """Given some content as input return some cleaned, validated content.
+ Typically raises a ErrorResponse with status code 400 (Bad Request) on failure.
+
+ Must be overridden to be implemented."""
+ return data
+
+ def object_to_data(self, obj):
+ return _object_to_data(obj)
+
+
+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
+
+ # TODO: Replace this with new Serializer code based on Forms API.
+ def object_to_data(self, obj):
+ """
+ A (horrible) munging of Piston's pre-serialization. Returns a dict.
+ """
+
+ return _object_to_data(obj)
+
+ def _any(thing, fields=()):
+ """
+ Dispatch, all types are routed through here.
+ """
+ ret = None
+
+ if isinstance(thing, QuerySet):
+ ret = _qs(thing, fields=fields)
+ elif isinstance(thing, (tuple, list)):
+ ret = _list(thing)
+ elif isinstance(thing, dict):
+ ret = _dict(thing)
+ elif isinstance(thing, int):
+ ret = thing
+ elif isinstance(thing, bool):
+ ret = thing
+ elif isinstance(thing, type(None)):
+ ret = thing
+ elif isinstance(thing, decimal.Decimal):
+ ret = str(thing)
+ elif isinstance(thing, models.Model):
+ ret = _model(thing, fields=fields)
+ #elif isinstance(thing, HttpResponse): TRC
+ # raise HttpStatusCode(thing)
+ elif inspect.isfunction(thing):
+ if not inspect.getargspec(thing)[0]:
+ ret = _any(thing())
+ elif hasattr(thing, '__rendertable__'):
+ f = thing.__rendertable__
+ if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
+ ret = _any(f())
+ else:
+ ret = unicode(thing) # TRC
+
+ return ret
+
+ def _fk(data, field):
+ """
+ Foreign keys.
+ """
+ return _any(getattr(data, field.name))
+
+ def _related(data, fields=()):
+ """
+ Foreign keys.
+ """
+ return [ _model(m, fields) for m in data.iterator() ]
+
+ def _m2m(data, field, fields=()):
+ """
+ Many to many (re-route to `_model`.)
+ """
+ return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
+
+
+ def _method_fields(data, fields):
+ if not data:
+ return { }
+
+ has = dir(data)
+ ret = dict()
+
+ for field in fields:
+ if field in has:
+ ret[field] = getattr(data, field)
+
+ return ret
+
+ def _model(data, fields=()):
+ """
+ Models. Will respect the `fields` and/or
+ `exclude` on the handler (see `typemapper`.)
+ """
+ ret = { }
+ #handler = self.in_typemapper(type(data), self.anonymous) # TRC
+ handler = None # TRC
+ get_absolute_url = False
+
+ if fields:
+ v = lambda f: getattr(data, f.attname)
+
+ get_fields = set(fields)
+ if 'absolute_url' in get_fields: # MOVED (TRC)
+ get_absolute_url = True
+
+ met_fields = _method_fields(handler, get_fields) # TRC
+
+ for f in data._meta.local_fields:
+ if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
+ if not f.rel:
+ 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:
+ if callable(maybe):
+ if len(inspect.getargspec(maybe)[0]) == 1:
+ ret[maybe_field] = _any(maybe())
+ else:
+ ret[maybe_field] = _any(maybe)
+ else:
+ pass # TRC
+ #handler_f = getattr(handler or self.handler, maybe_field, None)
+ #
+ #if handler_f:
+ # ret[maybe_field] = _any(handler_f(data))
+
+ else:
+ # Add absolute_url if it exists
+ get_absolute_url = True
+
+ # Add all the fields
+ for f in data._meta.fields:
+ if f.attname != 'id':
+ ret[f.attname] = _any(getattr(data, f.attname))
+
+ # Add all the propertiess
+ klass = data.__class__
+ for attr in dir(klass):
+ if not attr.startswith('_') and not attr in ('pk','id') and isinstance(getattr(klass, attr, None), property):
+ #if attr.endswith('_url') or attr.endswith('_uri'):
+ # ret[attr] = self.make_absolute(_any(getattr(data, attr)))
+ #else:
+ ret[attr] = _any(getattr(data, attr))
+ #fields = dir(data.__class__) + ret.keys()
+ #add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')]
+ #print add_ons
+ ###print dir(data.__class__)
+ #from django.db.models import Model
+ #model_fields = dir(Model)
+
+ #for attr in dir(data):
+ ## #if attr.startswith('_'):
+ ## # continue
+ # if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'):
+ # print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields
+
+ #for k in add_ons:
+ # ret[k] = _any(getattr(data, k))
+
+ # TRC
+ # 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()
+ # ret['resource_uri'] = permalink( lambda: (url_id,
+ # (getattr(data, f) for f in fields) ) )()
+
+ # TRC
+ #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_url:
+ try: ret['absolute_url'] = data.get_absolute_url()
+ except: pass
+
+ #for key, val in ret.items():
+ # if key.endswith('_url') or key.endswith('_uri'):
+ # ret[key] = self.add_domain(val)
+
+ return ret
+
+ def _qs(data, fields=()):
+ """
+ Querysets.
+ """
+ return [ _any(v, fields) for v in data ]
+
+ def _list(data):
+ """
+ Lists.
+ """
+ return [ _any(v) for v in data ]
+
+ def _dict(data):
+ """
+ Dictionaries.
+ """
+ return dict([ (k, _any(v)) for k, v in data.iteritems() ])
+
+ # Kickstart the seralizin'.
+ return _any(obj, self.fields)
+
+
+class FormResource(Resource):
+ """Validator class that uses forms for validation.
+ Also provides a get_bound_form() method which may be used by some renderers.
+
+ The view class should provide `.form` attribute which specifies the form classmethod
+ to be used for validation.
+
+ On calling validate() this validator may set a `.bound_form_instance` attribute on the
+ view, which may be used by some renderers."""
+
+
+ def validate(self, data, files):
+ """
+ Given some content as input return some cleaned, validated content.
+ Raises a 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 ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys.
+ If the '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)
+
+
+ def _validate(self, data, files, allowed_extra_fields=()):
+ """
+ Wrapped by validate to hide the extra_fields option that the ModelValidatorMixin uses.
+ extra_fields is a list of fields which are not defined by the form, but which we still
+ expect to see on the input.
+ """
+ bound_form = self.get_bound_form(data, files)
+
+ if bound_form is None:
+ return data
+
+ self.view.bound_form_instance = bound_form
+
+ 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)
+
+ # 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:
+ 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):
+ """Given some content return a Django form bound to that content.
+ If form validation is turned off (form class attribute is None) then returns None."""
+ form_cls = getattr(self, 'form', None)
+
+ if not form_cls:
+ return None
+
+ if data is not None:
+ return form_cls(data, files)
+
+ return form_cls()
+
+
+class ModelResource(FormResource):
+ """Validator class that uses forms for validation and otherwise falls back to a model form if no form is set.
+ Also provides a get_bound_form() method which may be used by some renderers."""
+
+ """The form class that should be used for validation, or None to use model form validation."""
+ form = None
+
+ """The model class from which the model form should be constructed if no form is set."""
+ model = None
+
+ """The list of fields we expect to receive as input. Fields in this list will may be received with
+ raising non-existent field errors, even if they do not exist as fields on the ModelForm.
+
+ Setting the fields class attribute causes the exclude_fields class attribute to be disregarded."""
+ fields = None
+
+ """The list of fields to exclude from the Model. This is only used if the fields class attribute is not set."""
+ exclude_fields = ('id', 'pk')
+
+
+ # TODO: test the different validation here to allow for get get_absolute_url to be supplied on input and not bork out
+ # TODO: be really strict on fields - check they match in the handler methods. (this isn't a validator thing tho.)
+ def validate(self, data, files):
+ """
+ Given some content as input return some cleaned, validated content.
+ Raises a 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 'errors' and 'field-errors' keys.
+ If the '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):
+ """Given some content return a Django form bound to that content.
+
+ If the form class attribute has been explicitly set then use that class to create a Form,
+ otherwise if model is set use that class to create a ModelForm, otherwise return None."""
+
+ form_cls = getattr(self, 'form', None)
+ model_cls = getattr(self, 'model', None)
+
+ if form_cls:
+ # Use explict Form
+ return super(ModelFormValidator, self).get_bound_form(data, files)
+
+ elif model_cls:
+ # Fall back to ModelForm which we create on the fly
+ class OnTheFlyModelForm(forms.ModelForm):
+ class Meta:
+ model = model_cls
+ #fields = tuple(self._model_fields_set)
+
+ # Instantiate the ModelForm as appropriate
+ if content and isinstance(content, models.Model):
+ # Bound to an existing model instance
+ return OnTheFlyModelForm(instance=content)
+ elif not data is None:
+ return OnTheFlyModelForm(data, files)
+ return OnTheFlyModelForm()
+
+ # Both form and model not set? Okay bruv, whatevs...
+ return None
+
+
+ @property
+ def _model_fields_set(self):
+ """Return a set containing the names of validated fields on the model."""
+ resource = self.view.resource
+ model = getattr(resource, 'model', None)
+ fields = getattr(resource, 'fields', self.fields)
+ exclude_fields = getattr(resource, 'exclude_fields', self.exclude_fields)
+
+ model_fields = set(field.name for field in model._meta.fields)
+
+ if fields:
+ return model_fields & set(as_tuple(fields))
+
+ return model_fields - set(as_tuple(exclude_fields))
+
+ @property
+ def _property_fields_set(self):
+ """Returns a set containing the names of validated properties on the model."""
+ resource = self.view.resource
+ model = getattr(resource, 'model', None)
+ fields = getattr(resource, 'fields', self.fields)
+ exclude_fields = getattr(resource, 'exclude_fields', self.exclude_fields)
+
+ property_fields = set(attr for attr in dir(model) if
+ isinstance(getattr(model, attr, None), property)
+ and not attr.startswith('_'))
+
+ if fields:
+ return property_fields & set(as_tuple(fields))
+
+ return property_fields - set(as_tuple(exclude_fields))