From f8c1481d4b927d8cd16406d805597ded44bb3f1c Mon Sep 17 00:00:00 2001 From: Nikolaus Schlemm Date: Sat, 18 May 2013 16:21:25 +0200 Subject: [PATCH 01/24] expose the fields metadata via OPTIONS as described in https://github.com/tomchristie/django-rest-framework/issues/192 missing: - get fields for method (currently all the same) - tests - right placement of code --- rest_framework/views.py | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 555fa2f40..2dd2c59e9 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -8,7 +8,7 @@ from django.views.decorators.csrf import csrf_exempt from rest_framework import status, exceptions from rest_framework.compat import View from rest_framework.response import Response -from rest_framework.request import Request +from rest_framework.request import clone_request, Request from rest_framework.settings import api_settings from rest_framework.utils.formatting import get_view_name, get_view_description @@ -52,19 +52,42 @@ class APIView(View): } def metadata(self, request): - return { + content = { 'name': get_view_name(self.__class__), 'description': get_view_description(self.__class__), 'renders': [renderer.media_type for renderer in self.renderer_classes], 'parses': [parser.media_type for parser in self.parser_classes], } - # TODO: Add 'fields', from serializer info, if it exists. - # serializer = self.get_serializer() - # if serializer is not None: - # field_name_types = {} - # for name, field in form.fields.iteritems(): - # field_name_types[name] = field.__class__.__name__ - # content['fields'] = field_name_types + action_metadata = self._generate_action_metadata(request) + if action_metadata is not None: + content['actions'] = action_metadata + + return content + + def _generate_action_metadata(self, request): + ''' + Helper for generating the fields metadata for allowed and permitted methods. + ''' + actions = {} + + for method in self.allowed_methods: + cloned_request = clone_request(request, method) + try: + self.check_permissions(cloned_request) + + # TODO: find right placement - APIView does not have get_serializer + serializer = self.get_serializer() + if serializer is not None: + field_name_types = {} + for name, field in serializer.fields.iteritems(): + field_name_types[name] = field.__class__.__name__ + + actions[method] = field_name_types + except: + # don't add this method + pass + + return actions if len(actions) > 0 else None def http_method_not_allowed(self, request, *args, **kwargs): """ From b4dbfa9832e2a29a5908ddf27f8746971a8e3c56 Mon Sep 17 00:00:00 2001 From: Nikolaus Schlemm Date: Sat, 18 May 2013 17:10:13 +0200 Subject: [PATCH 02/24] only catch relevant exceptions ;) --- rest_framework/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 2dd2c59e9..719df4284 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -83,7 +83,10 @@ class APIView(View): field_name_types[name] = field.__class__.__name__ actions[method] = field_name_types - except: + except exceptions.PermissionDenied: + # don't add this method + pass + except exceptions.NotAuthenticated: # don't add this method pass From a42afa04c38afe25c9032b8ce37b572678b02cf1 Mon Sep 17 00:00:00 2001 From: Nikolaus Schlemm Date: Sat, 18 May 2013 17:13:23 +0200 Subject: [PATCH 03/24] draft for fields' metadata via OPTIONS - needs review and decision --- rest_framework/tests/generics.py | 42 ++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 15d87e866..014195ae7 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -121,8 +121,27 @@ class TestRootView(TestCase): 'text/html' ], 'name': 'Root', - 'description': 'Example description for OPTIONS.' + 'description': 'Example description for OPTIONS.', + 'actions': {} } + # TODO: this is just a draft for fields' metadata - needs review and decision + for method in ('HEAD', 'GET', 'POST', 'OPTIONS'): + expected['actions'][method] = { + 'text': { + 'description': '', + 'label': '', + 'readonly': False, + 'required': True, + 'type': 'CharField', + }, + 'id': { + 'description': '', + 'label': '', + 'readonly': True, + 'required': True, + 'type': 'IntegerField', + }, + } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, expected) @@ -238,8 +257,27 @@ class TestInstanceView(TestCase): 'text/html' ], 'name': 'Instance', - 'description': 'Example description for OPTIONS.' + 'description': 'Example description for OPTIONS.', + 'actions': {} } + # TODO: this is just a draft idea for fields' metadata - needs review and decision + for method in ('HEAD', 'GET', 'PATCH', 'PUT', 'OPTIONS', 'DELETE'): + expected['actions'][method] = { + 'text': { + 'description': '', + 'label': '', + 'readonly': False, + 'required': True, + 'type': 'CharField', + }, + 'id': { + 'description': '', + 'label': '', + 'readonly': True, + 'required': True, + 'type': 'IntegerField', + }, + } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, expected) From 5ab7cc6e6be5445bc0d4ccc26f1ec84239af74d5 Mon Sep 17 00:00:00 2001 From: Nikolaus Schlemm Date: Sat, 18 May 2013 17:38:47 +0200 Subject: [PATCH 04/24] HEAD and OPTIONS should not be exposed as actions as discussed in https://github.com/nschlemm/django-rest-framework/commit/a42afa04c38afe25c9032b8ce37b572678b02cf1#commitcomment-3241476 --- rest_framework/tests/generics.py | 4 ++-- rest_framework/views.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 014195ae7..d8556638a 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -125,7 +125,7 @@ class TestRootView(TestCase): 'actions': {} } # TODO: this is just a draft for fields' metadata - needs review and decision - for method in ('HEAD', 'GET', 'POST', 'OPTIONS'): + for method in ('GET', 'POST',): expected['actions'][method] = { 'text': { 'description': '', @@ -261,7 +261,7 @@ class TestInstanceView(TestCase): 'actions': {} } # TODO: this is just a draft idea for fields' metadata - needs review and decision - for method in ('HEAD', 'GET', 'PATCH', 'PUT', 'OPTIONS', 'DELETE'): + for method in ('GET', 'PATCH', 'PUT', 'DELETE'): expected['actions'][method] = { 'text': { 'description': '', diff --git a/rest_framework/views.py b/rest_framework/views.py index 719df4284..c5b89a024 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -71,6 +71,10 @@ class APIView(View): actions = {} for method in self.allowed_methods: + # skip HEAD and OPTIONS + if method in ('HEAD', 'OPTIONS'): + continue + cloned_request = clone_request(request, method) try: self.check_permissions(cloned_request) From 4dffcb5d77a575793c1dc7c1db5242a3ac2d7345 Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sat, 18 May 2013 18:10:17 +0200 Subject: [PATCH 05/24] Added humanized field names and types --- rest_framework/fields.py | 91 +++++++++++++++++++++++++++++++++- rest_framework/tests/fields.py | 57 +++++++++++++++++++++ rest_framework/views.py | 2 + 3 files changed, 148 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 491aa7eda..b23813ec3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -23,13 +23,44 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.datastructures import SortedDict from rest_framework import ISO_8601 -from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time +from rest_framework.compat import (timezone, parse_date, parse_datetime, + parse_time) from rest_framework.compat import BytesIO from rest_framework.compat import six from rest_framework.compat import smart_text from rest_framework.settings import api_settings +HUMANIZED_FIELD_TYPES = { + 'BooleanField': u'Boolean', + 'CharField': u'Single Character', + 'ChoiceField': u'Single Choice', + 'ComboField': u'Single Choice', + 'DateField': u'Date', + 'DateTimeField': u'Date and Time', + 'DecimalField': u'Decimal', + 'EmailField': u'Email', + 'Field': u'Field', + 'FileField': u'File', + 'FilePathField': u'File Path', + 'FloatField': u'Float', + 'GenericIPAddressField': u'Generic IP Address', + 'IPAddressField': u'IP Address', + 'ImageField': u'Image', + 'IntegerField': u'Integer', + 'MultiValueField': u'Multiple Value', + 'MultipleChoiceField': u'Multiple Choice', + 'NullBooleanField': u'Nullable Boolean', + 'RegexField': u'Regular Expression', + 'SlugField': u'Slug', + 'SplitDateTimeField': u'Split Date and Time', + 'TimeField': u'Time', + 'TypedChoiceField': u'Typed Single Choice', + 'TypedMultipleChoiceField': u'Typed Multiple Choice', + 'URLField': u'URL', +} + + def is_simple_callable(obj): """ True if the object is a callable that takes no arguments. @@ -62,7 +93,8 @@ def get_component(obj, attr_name): def readable_datetime_formats(formats): - format = ', '.join(formats).replace(ISO_8601, 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]') + format = ', '.join(formats).replace(ISO_8601, + 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]') return humanize_strptime(format) @@ -71,6 +103,61 @@ def readable_date_formats(formats): return humanize_strptime(format) +def humanize_field_type(field_type): + """Return a human-readable name for a field type. + + :param field_type: Either a field type class (for example + django.forms.fields.DateTimeField), or the name of a field type + (for example "DateTimeField"). + + :return: unicode + + """ + if isinstance(field_type, basestring): + field_type_name = field_type + else: + field_type_name = field_type.__name__ + try: + return HUMANIZED_FIELD_TYPES[field_type_name] + except KeyError: + humanized = re.sub('([a-z0-9])([A-Z])', r'\1 \2', field_type_name) + return humanized.capitalize() + + +def humanize_field(field): + """Return a human-readable description of a field. + + :param field: A Django field. + + :return: A dictionary of the form {type: type name, required: bool, + label: field label: read_only: bool, + help_text: optional help text} + + """ + humanized = { + 'type': (field.type_name if field.type_name + else humanize_field_type(field.form_field_class)), + 'required': getattr(field, 'required', False), + 'label': field.label, + } + optional_attrs = ['read_only', 'help_text'] + for attr in optional_attrs: + if hasattr(field, attr): + humanized[attr] = getattr(field, attr) + return humanized + + +def humanize_form_fields(form): + """Return a humanized description of all the fields in a form. + + :param form: A Django form. + :return: A dictionary of {field_label: humanized description} + + """ + fields = SortedDict([(f.name, humanize_field(f)) for f in form.fields]) + return fields + + def readable_time_formats(formats): format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]') return humanize_strptime(format) diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 6b1cdfc77..de61b0112 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -4,12 +4,17 @@ General serializer field tests. from __future__ import unicode_literals from django.utils.datastructures import SortedDict import datetime +from rest_framework.fields import humanize_field, humanize_field_type +from django import forms from decimal import Decimal from django.db import models from django.test import TestCase from django.core import validators from rest_framework import serializers from rest_framework.serializers import Serializer +from rest_framework.fields import Field +from collections import namedtuple +from uuid import uuid4 class TimestampedModel(models.Model): @@ -685,3 +690,55 @@ class ChoiceFieldTests(TestCase): """ f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES) self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES) + + +class HumanizedFieldType(TestCase): + def test_standard_type_classes(self): + for field_type_name in forms.fields.__all__: + field_type = getattr(forms.fields, field_type_name) + humanized = humanize_field_type(field_type) + self.assert_valid_name(humanized) + + def test_standard_type_names(self): + for field_type_name in forms.fields.__all__: + humanized = humanize_field_type(field_type_name) + self.assert_valid_name(humanized) + + def test_custom_type_name(self): + humanized = humanize_field_type('SomeCustomType') + self.assertEquals(humanized, u'Some custom type') + + def test_custom_type(self): + custom_type = namedtuple('SomeCustomType', []) + humanized = humanize_field_type(custom_type) + self.assertEquals(humanized, u'Some custom type') + + def assert_valid_name(self, humanized): + """A humanized field name is valid if it's a non-empty + unicode. + + """ + self.assertIsInstance(humanized, unicode) + self.assertTrue(humanized) + + +class HumanizedField(TestCase): + def setUp(self): + self.required_field = Field() + self.required_field.label = uuid4().hex + self.required_field.required = True + + self.optional_field = Field() + self.optional_field.label = uuid4().hex + self.optional_field.required = False + + def test_required(self): + self.assertEqual(humanize_field(self.required_field)['required'], True) + + def test_optional(self): + self.assertEqual(humanize_field(self.optional_field)['required'], + False) + + def test_label(self): + for field in (self.required_field, self.optional_field): + self.assertEqual(humanize_field(field)['label'], field.label) diff --git a/rest_framework/views.py b/rest_framework/views.py index 2dd2c59e9..c4662a1f5 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -80,6 +80,8 @@ class APIView(View): if serializer is not None: field_name_types = {} for name, field in serializer.fields.iteritems(): + from rest_framework.fields import humanize_field + humanize_field(field) field_name_types[name] = field.__class__.__name__ actions[method] = field_name_types From fecadacab150aab48b8b84f4f0e5340ead74c287 Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sat, 18 May 2013 18:27:53 +0200 Subject: [PATCH 06/24] added tests for form --- rest_framework/fields.py | 6 +++--- rest_framework/tests/fields.py | 23 ++++++++++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index b23813ec3..544afc986 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -135,8 +135,7 @@ def humanize_field(field): """ humanized = { - 'type': (field.type_name if field.type_name - else humanize_field_type(field.form_field_class)), + 'type': humanize_field_type(field.__class__), 'required': getattr(field, 'required', False), 'label': field.label, } @@ -154,7 +153,8 @@ def humanize_form_fields(form): :return: A dictionary of {field_label: humanized description} """ - fields = SortedDict([(f.name, humanize_field(f)) for f in form.fields]) + fields = SortedDict([(name, humanize_field(field)) + for name, field in form.fields.iteritems()]) return fields diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index de61b0112..6a180cb89 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -4,7 +4,8 @@ General serializer field tests. from __future__ import unicode_literals from django.utils.datastructures import SortedDict import datetime -from rest_framework.fields import humanize_field, humanize_field_type +from rest_framework.fields import (humanize_field, humanize_field_type, + humanize_form_fields) from django import forms from decimal import Decimal from django.db import models @@ -742,3 +743,23 @@ class HumanizedField(TestCase): def test_label(self): for field in (self.required_field, self.optional_field): self.assertEqual(humanize_field(field)['label'], field.label) + + +class Form(forms.Form): + field1 = forms.CharField(max_length=3, label='field one') + field2 = forms.CharField(label='field two') + + +class HumanizedSerializer(TestCase): + def setUp(self): + self.serializer = TimestampedModelSerializer() + + def test_humanized(self): + humanized = humanize_form_fields(Form()) + self.assertEqual(humanized, { + 'field1': { + u'help_text': u'', u'required': True, + u'type': u'Single Character', u'label': 'field one'}, + 'field2': { + u'help_text': u'', u'required': True, + u'type': u'Single Character', u'label': 'field two'}}) From 7f1cc82f96c2ba4064b28957a8b2d5b313be3c40 Mon Sep 17 00:00:00 2001 From: Nikolaus Schlemm Date: Sat, 18 May 2013 18:29:51 +0200 Subject: [PATCH 07/24] added unittests for permission check of exposing actions via OPTIONS --- rest_framework/tests/permissions.py | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/rest_framework/tests/permissions.py b/rest_framework/tests/permissions.py index b3993be58..5a18182bf 100644 --- a/rest_framework/tests/permissions.py +++ b/rest_framework/tests/permissions.py @@ -108,6 +108,51 @@ class ModelPermissionsIntegrationTests(TestCase): response = instance_view(request, pk='2') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_options_permitted(self): + request = factory.options('/', content_type='application/json', + HTTP_AUTHORIZATION=self.permitted_credentials) + response = root_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('actions', response.data) + self.assertEquals(response.data['actions'].keys(), ['POST', 'GET',]) + + request = factory.options('/1', content_type='application/json', + HTTP_AUTHORIZATION=self.permitted_credentials) + response = instance_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('actions', response.data) + self.assertEquals(response.data['actions'].keys(), ['PUT', 'PATCH', 'DELETE', 'GET',]) + + def test_options_disallowed(self): + request = factory.options('/', content_type='application/json', + HTTP_AUTHORIZATION=self.disallowed_credentials) + response = root_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('actions', response.data) + self.assertEquals(response.data['actions'].keys(), ['GET',]) + + request = factory.options('/1', content_type='application/json', + HTTP_AUTHORIZATION=self.disallowed_credentials) + response = instance_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('actions', response.data) + self.assertEquals(response.data['actions'].keys(), ['GET',]) + + def test_options_updateonly(self): + request = factory.options('/', content_type='application/json', + HTTP_AUTHORIZATION=self.updateonly_credentials) + response = root_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('actions', response.data) + self.assertEquals(response.data['actions'].keys(), ['GET',]) + + request = factory.options('/1', content_type='application/json', + HTTP_AUTHORIZATION=self.updateonly_credentials) + response = instance_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('actions', response.data) + self.assertEquals(response.data['actions'].keys(), ['PUT', 'PATCH', 'GET',]) + class OwnerModel(models.Model): text = models.CharField(max_length=100) From c0f3a1c397a564ee78b3a656f14f7ff46b0d2b31 Mon Sep 17 00:00:00 2001 From: Nikolaus Schlemm Date: Sun, 19 May 2013 09:25:02 +0200 Subject: [PATCH 08/24] Integrated status quo of grimborg's awesome humanize_field() for exposing field metadata via OPTIONS :) --- rest_framework/fields.py | 2 +- rest_framework/tests/generics.py | 36 ++++++++++++++++---------------- rest_framework/views.py | 3 +-- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d6db3ebeb..a215e02b7 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -136,7 +136,7 @@ def humanize_field(field): humanized = { 'type': humanize_field_type(field.__class__), 'required': getattr(field, 'required', False), - 'label': field.label, + 'label': getattr(field, 'label', None), } optional_attrs = ['read_only', 'help_text'] for attr in optional_attrs: diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index d8556638a..a1edd28d7 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -128,18 +128,18 @@ class TestRootView(TestCase): for method in ('GET', 'POST',): expected['actions'][method] = { 'text': { - 'description': '', - 'label': '', - 'readonly': False, + #'description': '', + 'label': None, + 'read_only': False, 'required': True, - 'type': 'CharField', + 'type': 'Single Character', }, 'id': { - 'description': '', - 'label': '', - 'readonly': True, - 'required': True, - 'type': 'IntegerField', + #'description': '', + 'label': None, + 'read_only': True, + 'required': False, + 'type': 'Integer', }, } self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -264,18 +264,18 @@ class TestInstanceView(TestCase): for method in ('GET', 'PATCH', 'PUT', 'DELETE'): expected['actions'][method] = { 'text': { - 'description': '', - 'label': '', - 'readonly': False, + #'description': '', + 'label': None, + 'read_only': False, 'required': True, - 'type': 'CharField', + 'type': 'Single Character', }, 'id': { - 'description': '', - 'label': '', - 'readonly': True, - 'required': True, - 'type': 'IntegerField', + #'description': '', + 'label': None, + 'read_only': True, + 'required': False, + 'type': 'Integer', }, } self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/rest_framework/views.py b/rest_framework/views.py index 11d50e5dc..e8bd9f505 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -85,8 +85,7 @@ class APIView(View): field_name_types = {} for name, field in serializer.fields.iteritems(): from rest_framework.fields import humanize_field - humanize_field(field) - field_name_types[name] = field.__class__.__name__ + field_name_types[name] = humanize_field(field) actions[method] = field_name_types except exceptions.PermissionDenied: From b915c1d4d81fc459ed7c79ee5264ef7467963c3f Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sun, 19 May 2013 11:15:38 +0200 Subject: [PATCH 09/24] Made field label optional in OPTIONS --- rest_framework/fields.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a215e02b7..b1bbb4d4a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -136,11 +136,10 @@ def humanize_field(field): humanized = { 'type': humanize_field_type(field.__class__), 'required': getattr(field, 'required', False), - 'label': getattr(field, 'label', None), } - optional_attrs = ['read_only', 'help_text'] + optional_attrs = ['read_only', 'help_text', 'label'] for attr in optional_attrs: - if hasattr(field, attr): + if getattr(field, attr, None) is not None: humanized[attr] = getattr(field, attr) return humanized From 08e9e2042c99cde66721a22475df6e084a06e3d2 Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sun, 19 May 2013 11:26:56 +0200 Subject: [PATCH 10/24] Disabled label and help_text in unit test. We should change the test so that it sets them up. --- rest_framework/tests/generics.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index a1edd28d7..293c6633c 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -128,15 +128,17 @@ class TestRootView(TestCase): for method in ('GET', 'POST',): expected['actions'][method] = { 'text': { - #'description': '', - 'label': None, + # TODO add help_text and label when they are available + #'help_text': '', + #'label': None, 'read_only': False, 'required': True, 'type': 'Single Character', }, 'id': { - #'description': '', - 'label': None, + # TODO add help_text and label when they are available + #'help_text': '', + #'label': None, 'read_only': True, 'required': False, 'type': 'Integer', @@ -264,15 +266,19 @@ class TestInstanceView(TestCase): for method in ('GET', 'PATCH', 'PUT', 'DELETE'): expected['actions'][method] = { 'text': { + # TODO uncomment label and description when they are + # available #'description': '', - 'label': None, + #'label': None, 'read_only': False, 'required': True, 'type': 'Single Character', }, 'id': { + # TODO uncomment label and description when they are + # available #'description': '', - 'label': None, + #'label': None, 'read_only': True, 'required': False, 'type': 'Integer', From a91841f7fe91f79a18a1f4f0352ec3e07f389a0a Mon Sep 17 00:00:00 2001 From: Nikolaus Schlemm Date: Sun, 19 May 2013 11:29:41 +0200 Subject: [PATCH 11/24] WORKAROUND: avoid errors like "AttributeError: 'APIRoot' object has no attribute 'get_serializer'" --- rest_framework/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/views.py b/rest_framework/views.py index e8bd9f505..6348ca885 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -80,6 +80,8 @@ class APIView(View): self.check_permissions(cloned_request) # TODO: find right placement - APIView does not have get_serializer + if not hasattr(self, 'get_serializer'): + continue serializer = self.get_serializer() if serializer is not None: field_name_types = {} From 1154c12b3389c3098ee8755887f785da43666ec4 Mon Sep 17 00:00:00 2001 From: Nikolaus Schlemm Date: Sun, 19 May 2013 11:37:12 +0200 Subject: [PATCH 12/24] don't expose fields for GET and DELETE - leaving room for parameters like e.g. filter or paginate --- rest_framework/views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rest_framework/views.py b/rest_framework/views.py index 6348ca885..c9d55c18e 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -79,6 +79,11 @@ class APIView(View): try: self.check_permissions(cloned_request) + # TODO: discuss whether and how to expose parameters like e.g. filter or paginate + if method in ('GET', 'DELETE'): + actions[method] = {} + continue + # TODO: find right placement - APIView does not have get_serializer if not hasattr(self, 'get_serializer'): continue From b07cfdcf54961dc44604c6b87e2cc339901038a3 Mon Sep 17 00:00:00 2001 From: Nikolaus Schlemm Date: Sun, 19 May 2013 13:54:13 +0200 Subject: [PATCH 13/24] FIXED TEST FOR: don't expose fields for GET and DELETE - leaving room for parameters like e.g. filter or paginate --- rest_framework/tests/generics.py | 42 +++++++++++++++++--------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 293c6633c..2cbb55e6c 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -125,25 +125,25 @@ class TestRootView(TestCase): 'actions': {} } # TODO: this is just a draft for fields' metadata - needs review and decision - for method in ('GET', 'POST',): - expected['actions'][method] = { - 'text': { - # TODO add help_text and label when they are available - #'help_text': '', - #'label': None, - 'read_only': False, - 'required': True, - 'type': 'Single Character', - }, - 'id': { - # TODO add help_text and label when they are available - #'help_text': '', - #'label': None, - 'read_only': True, - 'required': False, - 'type': 'Integer', - }, - } + expected['actions']['GET'] = {} + expected['actions']['POST'] = { + 'text': { + # TODO add help_text and label when they are available + #'help_text': '', + #'label': None, + 'read_only': False, + 'required': True, + 'type': 'Single Character', + }, + 'id': { + # TODO add help_text and label when they are available + #'help_text': '', + #'label': None, + 'read_only': True, + 'required': False, + 'type': 'Integer', + }, + } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, expected) @@ -263,7 +263,9 @@ class TestInstanceView(TestCase): 'actions': {} } # TODO: this is just a draft idea for fields' metadata - needs review and decision - for method in ('GET', 'PATCH', 'PUT', 'DELETE'): + for method in ('GET', 'DELETE'): + expected['actions'][method] = {} + for method in ('PATCH', 'PUT'): expected['actions'][method] = { 'text': { # TODO uncomment label and description when they are From efca5f6feccf72421577ccea8889c7de729c47b8 Mon Sep 17 00:00:00 2001 From: Nikolaus Schlemm Date: Sun, 19 May 2013 14:54:16 +0200 Subject: [PATCH 14/24] use double quotes for docstring instead of single quotes --- rest_framework/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index c9d55c18e..be64a8d0e 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -65,9 +65,9 @@ class APIView(View): return content def _generate_action_metadata(self, request): - ''' + """ Helper for generating the fields metadata for allowed and permitted methods. - ''' + """ actions = {} for method in self.allowed_methods: From f1f5f92d899ae37d71234bf8a8a3ad52e20a253f Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sun, 19 May 2013 14:55:46 +0200 Subject: [PATCH 15/24] Added tests, cleaned up imports --- rest_framework/tests/fields.py | 20 +++++++++++--------- rest_framework/tests/views.py | 3 +++ rest_framework/views.py | 14 +++++--------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 7a5ed7181..6bc37db28 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -2,21 +2,23 @@ General serializer field tests. """ from __future__ import unicode_literals -from django.utils.datastructures import SortedDict -import datetime -from rest_framework.fields import (humanize_field, humanize_field_type, - humanize_form_fields) -from django import forms + +from collections import namedtuple from decimal import Decimal +from uuid import uuid4 + +import datetime +from django import forms +from django.core import validators from django.db import models from django.test import TestCase -from django.core import validators +from django.utils.datastructures import SortedDict + from rest_framework import serializers +from rest_framework.fields import (humanize_field, humanize_field_type, humanize_form_fields, + Field) from rest_framework.serializers import Serializer from rest_framework.tests.models import RESTFrameworkModel -from rest_framework.fields import Field -from collections import namedtuple -from uuid import uuid4 class TimestampedModel(models.Model): diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index 994cf6dc3..b70ef4ae2 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -6,6 +6,8 @@ from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView +from rest_framework import exceptions +from rest_framework import serializers import copy factory = RequestFactory() @@ -98,3 +100,4 @@ class FunctionBasedViewIntegrationTests(TestCase): } self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(sanitise_json_error(response.data), expected) + diff --git a/rest_framework/views.py b/rest_framework/views.py index c9d55c18e..1af4c0baa 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -2,13 +2,16 @@ Provides an APIView class that is the base of all views in REST framework. """ from __future__ import unicode_literals + from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponse from django.views.decorators.csrf import csrf_exempt + from rest_framework import status, exceptions from rest_framework.compat import View -from rest_framework.response import Response +from rest_framework.fields import humanize_form_fields from rest_framework.request import clone_request, Request +from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.utils.formatting import get_view_name, get_view_description @@ -69,7 +72,6 @@ class APIView(View): Helper for generating the fields metadata for allowed and permitted methods. ''' actions = {} - for method in self.allowed_methods: # skip HEAD and OPTIONS if method in ('HEAD', 'OPTIONS'): @@ -84,17 +86,11 @@ class APIView(View): actions[method] = {} continue - # TODO: find right placement - APIView does not have get_serializer if not hasattr(self, 'get_serializer'): continue serializer = self.get_serializer() if serializer is not None: - field_name_types = {} - for name, field in serializer.fields.iteritems(): - from rest_framework.fields import humanize_field - field_name_types[name] = humanize_field(field) - - actions[method] = field_name_types + actions[method] = humanize_form_fields(serializer) except exceptions.PermissionDenied: # don't add this method pass From 696c053f4fbbbb302d9b214d8daf511879256a7f Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sun, 19 May 2013 15:04:43 +0200 Subject: [PATCH 16/24] s/Single Character/String/ --- rest_framework/fields.py | 2 +- rest_framework/tests/fields.py | 4 ++-- rest_framework/tests/generics.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index b1bbb4d4a..98768d721 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -33,7 +33,7 @@ from rest_framework.settings import api_settings HUMANIZED_FIELD_TYPES = { 'BooleanField': u'Boolean', - 'CharField': u'Single Character', + 'CharField': u'String', 'ChoiceField': u'Single Choice', 'ComboField': u'Single Choice', 'DateField': u'Date', diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 6bc37db28..3882b53f4 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -885,7 +885,7 @@ class HumanizedSerializer(TestCase): self.assertEqual(humanized, { 'field1': { u'help_text': u'', u'required': True, - u'type': u'Single Character', u'label': 'field one'}, + u'type': u'String', u'label': 'field one'}, 'field2': { u'help_text': u'', u'required': True, - u'type': u'Single Character', u'label': 'field two'}}) + u'type': u'String', u'label': 'field two'}}) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 2cbb55e6c..38bf1990b 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -133,7 +133,7 @@ class TestRootView(TestCase): #'label': None, 'read_only': False, 'required': True, - 'type': 'Single Character', + 'type': 'String', }, 'id': { # TODO add help_text and label when they are available @@ -274,7 +274,7 @@ class TestInstanceView(TestCase): #'label': None, 'read_only': False, 'required': True, - 'type': 'Single Character', + 'type': 'String', }, 'id': { # TODO uncomment label and description when they are From edbf65c06e403645d91ce9dec24495a58323a3a0 Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sun, 19 May 2013 15:06:24 +0200 Subject: [PATCH 17/24] Removed unused imports --- rest_framework/tests/views.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index b70ef4ae2..c0f42a068 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -1,14 +1,16 @@ from __future__ import unicode_literals + +import copy + from django.test import TestCase from django.test.client import RequestFactory + from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView -from rest_framework import exceptions -from rest_framework import serializers -import copy + factory = RequestFactory() From e80488b6192580c7a731114e58edd718d1c79120 Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sun, 19 May 2013 15:08:41 +0200 Subject: [PATCH 18/24] Added min_length and max_length --- rest_framework/fields.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 98768d721..cdcb0ee9e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -137,7 +137,8 @@ def humanize_field(field): 'type': humanize_field_type(field.__class__), 'required': getattr(field, 'required', False), } - optional_attrs = ['read_only', 'help_text', 'label'] + optional_attrs = ['read_only', 'help_text', 'label', + 'min_length', 'max_length'] for attr in optional_attrs: if getattr(field, attr, None) is not None: humanized[attr] = getattr(field, attr) From 31893cff745b8cb62c3cc98b7103af2860eca319 Mon Sep 17 00:00:00 2001 From: Nikolaus Schlemm Date: Sun, 19 May 2013 15:12:01 +0200 Subject: [PATCH 19/24] * make test assertion more explicit * cleanup --- rest_framework/tests/fields.py | 2 +- rest_framework/tests/views.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 6bc37db28..d0b352413 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -846,7 +846,7 @@ class HumanizedFieldType(TestCase): """ self.assertIsInstance(humanized, unicode) - self.assertTrue(humanized) + self.assertNotEqual(humanized, '') class HumanizedField(TestCase): diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index b70ef4ae2..994cf6dc3 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -6,8 +6,6 @@ from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView -from rest_framework import exceptions -from rest_framework import serializers import copy factory = RequestFactory() @@ -100,4 +98,3 @@ class FunctionBasedViewIntegrationTests(TestCase): } self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(sanitise_json_error(response.data), expected) - From 7a5cd090aa91a5d1af0d54586f36e2156e1ccc60 Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sun, 19 May 2013 15:45:33 +0200 Subject: [PATCH 20/24] fixed tests, added docs, renamed helper method --- rest_framework/tests/fields.py | 22 +++++++++++++--------- rest_framework/tests/generics.py | 2 ++ rest_framework/views.py | 13 +++++++------ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 13df769f8..856980926 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -15,8 +15,8 @@ from django.test import TestCase from django.utils.datastructures import SortedDict from rest_framework import serializers -from rest_framework.fields import (humanize_field, humanize_field_type, humanize_form_fields, - Field) +from rest_framework.fields import (humanize_field, humanize_field_type, + humanize_form_fields, Field) from rest_framework.serializers import Serializer from rest_framework.tests.models import RESTFrameworkModel @@ -882,10 +882,14 @@ class HumanizedSerializer(TestCase): def test_humanized(self): humanized = humanize_form_fields(Form()) - self.assertEqual(humanized, { - 'field1': { - u'help_text': u'', u'required': True, - u'type': u'String', u'label': 'field one'}, - 'field2': { - u'help_text': u'', u'required': True, - u'type': u'String', u'label': 'field two'}}) + expected = { + 'field1': {u'help_text': u'', + u'label': u'field one', + u'max_length': 3, + u'required': True, + u'type': u'String'}, + 'field2': {u'help_text': u'', + u'label': u'field two', + u'required': True, + u'type': u'String'}} + self.assertEqual(humanized, expected) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 38bf1990b..b7292c814 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -131,6 +131,7 @@ class TestRootView(TestCase): # TODO add help_text and label when they are available #'help_text': '', #'label': None, + 'max_length': 100, 'read_only': False, 'required': True, 'type': 'String', @@ -272,6 +273,7 @@ class TestInstanceView(TestCase): # available #'description': '', #'label': None, + 'max_length': 100, 'read_only': False, 'required': True, 'type': 'String', diff --git a/rest_framework/views.py b/rest_framework/views.py index 1c4854f05..5f9e1bf2c 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -61,15 +61,16 @@ class APIView(View): 'renders': [renderer.media_type for renderer in self.renderer_classes], 'parses': [parser.media_type for parser in self.parser_classes], } - action_metadata = self._generate_action_metadata(request) - if action_metadata is not None: - content['actions'] = action_metadata + content['actions'] = self.action_metadata(request) return content - def _generate_action_metadata(self, request): - """ - Helper for generating the fields metadata for allowed and permitted methods. + def action_metadata(self, request): + """Return a dictionary with the fields required fo reach allowed method. If no method is allowed, + return an empty dictionary. + + :param request: Request for which to return the metadata of the allowed methods. + :return: A dictionary of the form {method: {field: {field attribute: value}}} """ actions = {} for method in self.allowed_methods: From 9133ef77ab7940abbf69b688cd3b28c62665a8c7 Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sun, 19 May 2013 15:47:23 +0200 Subject: [PATCH 21/24] removed TODO --- rest_framework/tests/generics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index b7292c814..8285e2d18 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -124,7 +124,6 @@ class TestRootView(TestCase): 'description': 'Example description for OPTIONS.', 'actions': {} } - # TODO: this is just a draft for fields' metadata - needs review and decision expected['actions']['GET'] = {} expected['actions']['POST'] = { 'text': { From 259153b3f96f51a613c0526ab947517a2a774857 Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sun, 19 May 2013 15:47:50 +0200 Subject: [PATCH 22/24] removed TODO --- rest_framework/tests/generics.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 8285e2d18..e6b3797bb 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -262,26 +262,17 @@ class TestInstanceView(TestCase): 'description': 'Example description for OPTIONS.', 'actions': {} } - # TODO: this is just a draft idea for fields' metadata - needs review and decision for method in ('GET', 'DELETE'): expected['actions'][method] = {} for method in ('PATCH', 'PUT'): expected['actions'][method] = { 'text': { - # TODO uncomment label and description when they are - # available - #'description': '', - #'label': None, 'max_length': 100, 'read_only': False, 'required': True, 'type': 'String', }, 'id': { - # TODO uncomment label and description when they are - # available - #'description': '', - #'label': None, 'read_only': True, 'required': False, 'type': 'Integer', From dea0b6ab7fcf8eb9ffebbe7720e4923df7f3c5a4 Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Sun, 19 May 2013 15:48:16 +0200 Subject: [PATCH 23/24] removed TODO --- rest_framework/tests/generics.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index e6b3797bb..a2f8fb4b3 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -127,18 +127,12 @@ class TestRootView(TestCase): expected['actions']['GET'] = {} expected['actions']['POST'] = { 'text': { - # TODO add help_text and label when they are available - #'help_text': '', - #'label': None, 'max_length': 100, 'read_only': False, 'required': True, 'type': 'String', }, 'id': { - # TODO add help_text and label when they are available - #'help_text': '', - #'label': None, 'read_only': True, 'required': False, 'type': 'Integer', From a1deb5eac7d6d00c6269d88fce1cc6818d8ec04a Mon Sep 17 00:00:00 2001 From: Oscar Vilaplana Date: Thu, 23 May 2013 08:26:55 +0200 Subject: [PATCH 24/24] simplified, moved field humanizing to Field. broken tests --- rest_framework/fields.py | 86 +++++--------------------------- rest_framework/serializers.py | 7 +++ rest_framework/tests/fields.py | 91 +++++++++++++--------------------- rest_framework/views.py | 2 +- 4 files changed, 56 insertions(+), 130 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index cdcb0ee9e..d5a1394dc 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -31,36 +31,6 @@ from rest_framework.compat import smart_text, force_text, is_non_str_iterable from rest_framework.settings import api_settings -HUMANIZED_FIELD_TYPES = { - 'BooleanField': u'Boolean', - 'CharField': u'String', - 'ChoiceField': u'Single Choice', - 'ComboField': u'Single Choice', - 'DateField': u'Date', - 'DateTimeField': u'Date and Time', - 'DecimalField': u'Decimal', - 'EmailField': u'Email', - 'Field': u'Field', - 'FileField': u'File', - 'FilePathField': u'File Path', - 'FloatField': u'Float', - 'GenericIPAddressField': u'Generic IP Address', - 'IPAddressField': u'IP Address', - 'ImageField': u'Image', - 'IntegerField': u'Integer', - 'MultiValueField': u'Multiple Value', - 'MultipleChoiceField': u'Multiple Choice', - 'NullBooleanField': u'Nullable Boolean', - 'RegexField': u'Regular Expression', - 'SlugField': u'Slug', - 'SplitDateTimeField': u'Split Date and Time', - 'TimeField': u'Time', - 'TypedChoiceField': u'Typed Single Choice', - 'TypedMultipleChoiceField': u'Typed Multiple Choice', - 'URLField': u'URL', -} - - def is_simple_callable(obj): """ True if the object is a callable that takes no arguments. @@ -102,49 +72,6 @@ def readable_date_formats(formats): return humanize_strptime(format) -def humanize_field_type(field_type): - """Return a human-readable name for a field type. - - :param field_type: Either a field type class (for example - django.forms.fields.DateTimeField), or the name of a field type - (for example "DateTimeField"). - - :return: unicode - - """ - if isinstance(field_type, basestring): - field_type_name = field_type - else: - field_type_name = field_type.__name__ - try: - return HUMANIZED_FIELD_TYPES[field_type_name] - except KeyError: - humanized = re.sub('([a-z0-9])([A-Z])', r'\1 \2', field_type_name) - return humanized.capitalize() - - -def humanize_field(field): - """Return a human-readable description of a field. - - :param field: A Django field. - - :return: A dictionary of the form {type: type name, required: bool, - label: field label: read_only: bool, - help_text: optional help text} - - """ - humanized = { - 'type': humanize_field_type(field.__class__), - 'required': getattr(field, 'required', False), - } - optional_attrs = ['read_only', 'help_text', 'label', - 'min_length', 'max_length'] - for attr in optional_attrs: - if getattr(field, attr, None) is not None: - humanized[attr] = getattr(field, attr) - return humanized - - def humanize_form_fields(form): """Return a humanized description of all the fields in a form. @@ -274,6 +201,19 @@ class Field(object): return {'type': self.type_name} return {} + @property + def humanized(self): + humanized = { + 'type': self.type_name, + 'required': getattr(self, 'required', False), + } + optional_attrs = ['read_only', 'help_text', 'label', + 'min_length', 'max_length'] + for attr in optional_attrs: + if getattr(self, attr, None) is not None: + humanized[attr] = getattr(self, attr) + return humanized + class WritableField(Field): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 943fba6ba..072815df2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -521,6 +521,13 @@ class BaseSerializer(WritableField): return self.object + @property + def humanized(self): + humanized_fields = SortedDict( + [(name, field.humanized) + for name, field in self.fields.iteritems()]) + return humanized_fields + class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)): pass diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 856980926..fd1fe9615 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -15,8 +15,7 @@ from django.test import TestCase from django.utils.datastructures import SortedDict from rest_framework import serializers -from rest_framework.fields import (humanize_field, humanize_field_type, - humanize_form_fields, Field) +from rest_framework.fields import Field, CharField from rest_framework.serializers import Serializer from rest_framework.tests.models import RESTFrameworkModel @@ -768,14 +767,16 @@ class SlugFieldTests(TestCase): def test_given_serializer_value(self): class SlugFieldSerializer(serializers.ModelSerializer): - slug_field = serializers.SlugField(source='slug_field', max_length=20, required=False) + slug_field = serializers.SlugField(source='slug_field', + max_length=20, required=False) class Meta: model = self.SlugFieldModel serializer = SlugFieldSerializer(data={}) self.assertEqual(serializer.is_valid(), True) - self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 20) + self.assertEqual(getattr(serializer.fields['slug_field'], + 'max_length'), 20) class URLFieldTests(TestCase): @@ -796,7 +797,8 @@ class URLFieldTests(TestCase): serializer = URLFieldSerializer(data={}) self.assertEqual(serializer.is_valid(), True) - self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 200) + self.assertEqual(getattr(serializer.fields['url_field'], + 'max_length'), 200) def test_given_model_value(self): class URLFieldSerializer(serializers.ModelSerializer): @@ -805,48 +807,21 @@ class URLFieldTests(TestCase): serializer = URLFieldSerializer(data={}) self.assertEqual(serializer.is_valid(), True) - self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 128) + self.assertEqual(getattr(serializer.fields['url_field'], + 'max_length'), 128) def test_given_serializer_value(self): class URLFieldSerializer(serializers.ModelSerializer): - url_field = serializers.URLField(source='url_field', max_length=20, required=False) + url_field = serializers.URLField(source='url_field', + max_length=20, required=False) class Meta: model = self.URLFieldWithGivenMaxLengthModel serializer = URLFieldSerializer(data={}) self.assertEqual(serializer.is_valid(), True) - self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20) - - -class HumanizedFieldType(TestCase): - def test_standard_type_classes(self): - for field_type_name in forms.fields.__all__: - field_type = getattr(forms.fields, field_type_name) - humanized = humanize_field_type(field_type) - self.assert_valid_name(humanized) - - def test_standard_type_names(self): - for field_type_name in forms.fields.__all__: - humanized = humanize_field_type(field_type_name) - self.assert_valid_name(humanized) - - def test_custom_type_name(self): - humanized = humanize_field_type('SomeCustomType') - self.assertEquals(humanized, u'Some custom type') - - def test_custom_type(self): - custom_type = namedtuple('SomeCustomType', []) - humanized = humanize_field_type(custom_type) - self.assertEquals(humanized, u'Some custom type') - - def assert_valid_name(self, humanized): - """A humanized field name is valid if it's a non-empty - unicode. - - """ - self.assertIsInstance(humanized, unicode) - self.assertNotEqual(humanized, '') + self.assertEqual(getattr(serializer.fields['url_field'], + 'max_length'), 20) class HumanizedField(TestCase): @@ -859,37 +834,41 @@ class HumanizedField(TestCase): self.optional_field.label = uuid4().hex self.optional_field.required = False + def test_type(self): + for field in (self.required_field, self.optional_field): + self.assertEqual(field.humanized['type'], field.type_name) + def test_required(self): - self.assertEqual(humanize_field(self.required_field)['required'], True) + self.assertEqual(self.required_field.humanized['required'], True) def test_optional(self): - self.assertEqual(humanize_field(self.optional_field)['required'], - False) + self.assertEqual(self.optional_field.humanized['required'], False) def test_label(self): for field in (self.required_field, self.optional_field): - self.assertEqual(humanize_field(field)['label'], field.label) + self.assertEqual(field.humanized['label'], field.label) -class Form(forms.Form): - field1 = forms.CharField(max_length=3, label='field one') - field2 = forms.CharField(label='field two') +class HumanizableSerializer(Serializer): + field1 = CharField(3, required=True) + field2 = CharField(10, required=False) class HumanizedSerializer(TestCase): def setUp(self): - self.serializer = TimestampedModelSerializer() + self.serializer = HumanizableSerializer() def test_humanized(self): - humanized = humanize_form_fields(Form()) + humanized = self.serializer.humanized expected = { - 'field1': {u'help_text': u'', - u'label': u'field one', + 'field1': {u'required': True, u'max_length': 3, - u'required': True, - u'type': u'String'}, - 'field2': {u'help_text': u'', - u'label': u'field two', - u'required': True, - u'type': u'String'}} - self.assertEqual(humanized, expected) + u'type': u'CharField', + u'read_only': False}, + 'field2': {u'required': False, + u'max_length': 10, + u'type': u'CharField', + u'read_only': False}} + self.assertEqual(set(expected.keys()), set(humanized.keys())) + for k, v in humanized.iteritems(): + self.assertEqual(v, expected[k]) diff --git a/rest_framework/views.py b/rest_framework/views.py index 5f9e1bf2c..d1afbe894 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -91,7 +91,7 @@ class APIView(View): continue serializer = self.get_serializer() if serializer is not None: - actions[method] = humanize_form_fields(serializer) + actions[method] = serializer.humanized except exceptions.PermissionDenied: # don't add this method pass