diff options
| author | Tom Christie | 2011-05-12 12:55:13 +0100 |
|---|---|---|
| committer | Tom Christie | 2011-05-12 12:55:13 +0100 |
| commit | 15f9e7c56699d31043782045a9fe47c354f612cb (patch) | |
| tree | 2c58441416a877d0afba22d85aea691190a17fa1 /djangorestframework/resource.py | |
| parent | 4d126796752cc3c79a24fd9caed49da6c525096f (diff) | |
| download | django-rest-framework-15f9e7c56699d31043782045a9fe47c354f612cb.tar.bz2 | |
refactoring resource specfic stuff into ResourceMixin - validators now defunct
Diffstat (limited to 'djangorestframework/resource.py')
| -rw-r--r-- | djangorestframework/resource.py | 273 |
1 files changed, 250 insertions, 23 deletions
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py index 44178684..775d5288 100644 --- a/djangorestframework/resource.py +++ b/djangorestframework/resource.py @@ -42,13 +42,13 @@ def _object_to_data(obj): return [_object_to_data(item) for item in obj] if isinstance(obj, models.Manager): # Manager objects - ret = [_object_to_data(item) for item in obj.all()] + 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) + return str(obj) if inspect.isfunction(obj) and not inspect.getargspec(obj)[0]: # function with no args return _object_to_data(obj()) @@ -60,26 +60,48 @@ def _object_to_data(obj): return smart_unicode(obj, strings_only=True) -# TODO: Replace this with new Serializer code based on Forms API. +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 Resource(object): -# def __init__(self, view): -# self.view = view -# -# def object_to_data(self, obj): -# pass -# -# def data_to_object(self, data, files): -# pass -# -#class FormResource(object): -# pass -# -#class ModelResource(object): -# pass +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(object): + +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. @@ -99,9 +121,11 @@ class Resource(object): # you should explicitly set the fields attribute on your class. fields = None - @classmethod - def object_to_serializable(self, data): - """A (horrible) munging of Piston's pre-serialization. Returns a dict""" + # 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. + """ def _any(thing, fields=()): """ @@ -321,5 +345,208 @@ class Resource(object): return dict([ (k, _any(v)) for k, v in data.iteritems() ]) # Kickstart the seralizin'. - return _any(data, self.fields) + 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)) |
