From ec096a1caceff6a4f5c75a152dd1c7bea9ed281d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Sep 2014 15:07:56 +0100 Subject: [PATCH] Add relations and get tests running --- rest_framework/fields.py | 30 +++++++-- rest_framework/mixins.py | 1 - rest_framework/relations.py | 111 ++++++++++++++++++++++++++++++++++ rest_framework/renderers.py | 14 ++--- rest_framework/serializers.py | 8 +-- rest_framework/utils/html.py | 2 + tests/test_serializer.py | 4 +- 7 files changed, 151 insertions(+), 19 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a83bf94c4..3e0f7ca47 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -68,7 +68,7 @@ class Field(object): def __init__(self, read_only=False, write_only=False, required=None, default=empty, initial=None, source=None, - label=None, style=None): + label=None, style=None, error_messages=None): self._creation_counter = Field._creation_counter Field._creation_counter += 1 @@ -216,9 +216,11 @@ class CharField(Field): 'blank': 'This field may not be blank.' } - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self.allow_blank = kwargs.pop('allow_blank', False) - super(CharField, self).__init__(*args, **kwargs) + self.max_length = kwargs.pop('max_length', None) + self.min_length = kwargs.pop('min_length', None) + super(CharField, self).__init__(**kwargs) def to_native(self, data): if data == '' and not self.allow_blank: @@ -233,7 +235,7 @@ class ChoiceField(Field): } coerce_to_type = str - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): choices = kwargs.pop('choices') assert choices, '`choices` argument is required and may not be empty' @@ -257,7 +259,7 @@ class ChoiceField(Field): str(key): key for key in self.choices.keys() } - super(ChoiceField, self).__init__(*args, **kwargs) + super(ChoiceField, self).__init__(**kwargs) def to_native(self, data): try: @@ -296,6 +298,24 @@ class IntegerField(Field): return data +class EmailField(CharField): + pass # TODO + + +class RegexField(CharField): + def __init__(self, **kwargs): + self.regex = kwargs.pop('regex') + super(CharField, self).__init__(**kwargs) + + +class DateTimeField(CharField): + pass # TODO + + +class FileField(Field): + pass # TODO + + class MethodField(Field): def __init__(self, **kwargs): kwargs['source'] = '*' diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index ee01cabc7..3e9c9bb33 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -6,7 +6,6 @@ which allows mixin classes to be composed in interesting ways. """ from __future__ import unicode_literals -from django.core.exceptions import ValidationError from django.http import Http404 from rest_framework import status from rest_framework.response import Response diff --git a/rest_framework/relations.py b/rest_framework/relations.py index e69de29bb..42d2c121a 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -0,0 +1,111 @@ +from rest_framework.fields import Field +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import resolve, get_script_prefix +from rest_framework.compat import urlparse + + +def get_default_queryset(serializer_class, field_name): + manager = getattr(serializer_class.opts.model, field_name) + if hasattr(manager, 'related'): + # Forward relationships + return manager.related.model._default_manager.all() + # Reverse relationships + return manager.field.rel.to._default_manager.all() + + +class RelatedField(Field): + def __init__(self, **kwargs): + self.queryset = kwargs.pop('queryset', None) + self.many = kwargs.pop('many', False) + super(RelatedField, self).__init__(**kwargs) + + def bind(self, field_name, parent, root): + super(RelatedField, self).bind(field_name, parent, root) + if self.queryset is None and not self.read_only: + self.queryset = get_default_queryset(parent, self.source) + + +class PrimaryKeyRelatedField(RelatedField): + MESSAGES = { + 'required': 'This field is required.', + 'does_not_exist': "Invalid pk '{pk_value}' - object does not exist.", + 'incorrect_type': 'Incorrect type. Expected pk value, received {data_type}.', + } + + def from_native(self, data): + try: + return self.queryset.get(pk=data) + except ObjectDoesNotExist: + self.fail('does_not_exist', pk_value=data) + except (TypeError, ValueError): + self.fail('incorrect_type', data_type=type(data).__name__) + + +class HyperlinkedRelatedField(RelatedField): + lookup_field = 'pk' + + MESSAGES = { + 'required': 'This field is required.', + 'no_match': 'Invalid hyperlink - No URL match', + 'incorrect_match': 'Invalid hyperlink - Incorrect URL match.', + 'does_not_exist': "Invalid hyperlink - Object does not exist.", + 'incorrect_type': 'Incorrect type. Expected URL string, received {data_type}.', + } + + def __init__(self, **kwargs): + self.view_name = kwargs.pop('view_name') + self.lookup_field = kwargs.pop('lookup_field', self.lookup_field) + self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field) + super(HyperlinkedRelatedField, self).__init__(**kwargs) + + def get_object(self, view_name, view_args, view_kwargs): + """ + Return the object corresponding to a matched URL. + + Takes the matched URL conf arguments, and should return an + object instance, or raise an `ObjectDoesNotExist` exception. + """ + lookup_value = view_kwargs[self.lookup_url_kwarg] + lookup_kwargs = {self.lookup_field: lookup_value} + return self.queryset.get(**lookup_kwargs) + + def from_native(self, value): + try: + http_prefix = value.startswith(('http:', 'https:')) + except AttributeError: + self.fail('incorrect_type', type(value).__name__) + + if http_prefix: + # If needed convert absolute URLs to relative path + value = urlparse.urlparse(value).path + prefix = get_script_prefix() + if value.startswith(prefix): + value = '/' + value[len(prefix):] + + try: + match = resolve(value) + except Exception: + self.fail('no_match') + + if match.view_name != self.view_name: + self.fail('incorrect_match') + + try: + return self.get_object(match.view_name, match.args, match.kwargs) + except (ObjectDoesNotExist, TypeError, ValueError): + self.fail('does_not_exist') + + +class HyperlinkedIdentityField(RelatedField): + lookup_field = 'pk' + + def __init__(self, **kwargs): + self.view_name = kwargs.pop('view_name') + self.lookup_field = kwargs.pop('lookup_field', self.lookup_field) + self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field) + super(HyperlinkedIdentityField, self).__init__(**kwargs) + + +class SlugRelatedField(RelatedField): + def __init__(self, **kwargs): + self.slug_field = kwargs.pop('slug_field', None) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index e8935b012..dfc5a39f6 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -436,13 +436,13 @@ class BrowsableAPIRenderer(BaseRenderer): if request.method == method: try: data = request.DATA - files = request.FILES + # files = request.FILES except ParseError: data = None - files = None + # files = None else: data = None - files = None + # files = None with override_method(view, request, method) as request: obj = getattr(view, 'object', None) @@ -579,10 +579,10 @@ class BrowsableAPIRenderer(BaseRenderer): 'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes], 'response_headers': response_headers, - #'put_form': self.get_rendered_html_form(view, 'PUT', request), - #'post_form': self.get_rendered_html_form(view, 'POST', request), - #'delete_form': self.get_rendered_html_form(view, 'DELETE', request), - #'options_form': self.get_rendered_html_form(view, 'OPTIONS', request), + # 'put_form': self.get_rendered_html_form(view, 'PUT', request), + # 'post_form': self.get_rendered_html_form(view, 'POST', request), + # 'delete_form': self.get_rendered_html_form(view, 'DELETE', request), + # 'options_form': self.get_rendered_html_form(view, 'OPTIONS', request), 'raw_data_put_form': raw_data_put_form, 'raw_data_post_form': raw_data_post_form, diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d121812d6..2f23b4d9a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -477,8 +477,8 @@ class ModelSerializer(Serializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) - # if model_field.help_text is not None: - # kwargs['help_text'] = model_field.help_text + # if model_field.help_text is not None: + # kwargs['help_text'] = model_field.help_text if model_field.verbose_name is not None: kwargs['label'] = model_field.verbose_name if not model_field.editable: @@ -566,8 +566,8 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): class HyperlinkedModelSerializer(ModelSerializer): _options_class = HyperlinkedModelSerializerOptions _default_view_name = '%(model_name)s-detail' - #_hyperlink_field_class = HyperlinkedRelatedField - #_hyperlink_identify_field_class = HyperlinkedIdentityField + # _hyperlink_field_class = HyperlinkedRelatedField + # _hyperlink_identify_field_class = HyperlinkedIdentityField def get_default_fields(self): fields = super(HyperlinkedModelSerializer, self).get_default_fields() diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py index bf17050df..edc591e9c 100644 --- a/rest_framework/utils/html.py +++ b/rest_framework/utils/html.py @@ -1,6 +1,8 @@ """ Helpers for dealing with HTML input. """ +import re + def is_html_input(dictionary): # MultiDict type datastructures are used to represent HTML form input, diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 90f37cf2e..fa5cafcfb 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -1795,8 +1795,8 @@ class DefaultValuesOnAutogeneratedFieldsTests(TestCase): class MetadataSerializer(serializers.Serializer): - field1 = serializers.CharField(3, required=True) - field2 = serializers.CharField(10, required=False) + field1 = serializers.CharField(max_length=3, required=True) + field2 = serializers.CharField(max_length=10, required=False) class MetadataSerializerTestCase(TestCase):