diff --git a/rest_framework/fields.py b/rest_framework/fields.py index fc14184cc..46fb31528 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -110,7 +110,7 @@ class Field(object): use_files = False form_field_class = forms.CharField - def __init__(self, source=None): + def __init__(self, source=None, label=None, help_text=None): self.parent = None self.creation_counter = Field.creation_counter @@ -118,6 +118,12 @@ class Field(object): self.source = source + if label is not None: + self.label = smart_text(label) + + if help_text is not None: + self.help_text = smart_text(help_text) + def initialize(self, parent, field_name): """ Called to set up a field prior to field_to_native or field_from_native. @@ -200,7 +206,8 @@ class WritableField(Field): widget = widgets.TextInput default = None - def __init__(self, source=None, read_only=False, required=None, + def __init__(self, source=None, label=None, help_text=None, + read_only=False, required=None, validators=[], error_messages=None, widget=None, default=None, blank=None): @@ -211,7 +218,7 @@ class WritableField(Field): DeprecationWarning, stacklevel=2) required = not(blank) - super(WritableField, self).__init__(source=source) + super(WritableField, self).__init__(source=source, label=label, help_text=help_text) self.read_only = read_only if required is None: diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8361cd409..2b56d6e41 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -366,7 +366,11 @@ class BrowsableAPIRenderer(BaseRenderer): if getattr(v, 'default', None) is not None: kwargs['initial'] = v.default - kwargs['label'] = k + if getattr(v, 'label', None) is not None: + kwargs['label'] = v.label + + if getattr(v, 'help_text', None) is not None: + kwargs['help_text'] = v.help_text fields[k] = v.form_field_class(**kwargs) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ff5eb8732..425dd18cc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -744,6 +744,12 @@ class ModelSerializer(Serializer): kwargs['choices'] = model_field.flatchoices return ChoiceField(**kwargs) + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name + + if model_field.help_text is not None: + kwargs['help_text'] = model_field.help_text + attribute_dict = { models.CharField: ['max_length'], models.CommaSeparatedIntegerField: ['max_length'], diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 40e41a644..abf50a2de 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals from django.db import models +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers def foobar(): @@ -32,7 +34,7 @@ class Anchor(RESTFrameworkModel): class BasicModel(RESTFrameworkModel): - text = models.CharField(max_length=100) + text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description.")) class SlugBasedModel(RESTFrameworkModel): @@ -159,3 +161,9 @@ class NullableOneToOneSource(RESTFrameworkModel): name = models.CharField(max_length=100) target = models.OneToOneField(OneToOneTarget, null=True, blank=True, related_name='nullable_source') + +# Serializer used to test BasicModel +class BasicModelSerializer(serializers.ModelSerializer): + class Meta: + model = BasicModel + diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py index aecf83f4e..d49aaa1f9 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -1,14 +1,18 @@ from __future__ import unicode_literals from django.test import TestCase +from rest_framework.tests.models import BasicModel, BasicModelSerializer from rest_framework.compat import patterns, url, include from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework import generics +from rest_framework import routers from rest_framework import status from rest_framework.renderers import ( BaseRenderer, JSONRenderer, BrowsableAPIRenderer ) +from rest_framework import viewsets from rest_framework.settings import api_settings from rest_framework.compat import six @@ -65,11 +69,27 @@ class HTMLView1(APIView): return Response('text') +class HTMLNewModelViewSet(viewsets.ModelViewSet): + model = BasicModel + + +class HTMLNewModelView(generics.ListCreateAPIView): + renderer_classes = (BrowsableAPIRenderer,) + permission_classes = [] + serializer_class = BasicModelSerializer + model = BasicModel + + +new_model_viewset_router = routers.DefaultRouter() +new_model_viewset_router.register(r'', HTMLNewModelViewSet) + urlpatterns = patterns('', url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), + url(r'^html_new_model$', HTMLNewModelView.as_view()), + url(r'^html_new_model_viewset', include(new_model_viewset_router.urls)), url(r'^restframework', include('rest_framework.urls', namespace='rest_framework')) ) @@ -173,3 +193,28 @@ class Issue122Tests(TestCase): Test if no infinite recursion occurs. """ self.client.get('/html1') + + +class Issue467Tests(TestCase): + """ + Tests for #467 + """ + + urls = 'rest_framework.tests.response' + + def test_viewset_label_help_text(self): + param = '?%s=%s' % ( + api_settings.URL_ACCEPT_OVERRIDE, + 'text/html' + ) + resp = self.client.get('/html_new_model_viewset/' + param) + self.assertEqual(resp['Content-Type'], 'text/html') + self.assertContains(resp, 'Text comes here') + self.assertContains(resp, 'Text description.') + + def test_form_has_label_and_help_text(self): + resp = self.client.get('/html_new_model') + self.assertEqual(resp['Content-Type'], 'text/html') + self.assertContains(resp, 'Text comes here') + self.assertContains(resp, 'Text description.') + diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 4f188c3e4..8f901a85e 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -3,10 +3,11 @@ from django.db import models from django.db.models.fields import BLANK_CHOICE_DASH from django.utils.datastructures import MultiValueDict from django.test import TestCase -from rest_framework import serializers +from rest_framework import serializers, fields, relations from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel) +from rest_framework.tests.models import BasicModelSerializer import datetime import pickle @@ -1323,6 +1324,30 @@ class DeserializeListTestCase(TestCase): self.assertEqual(serializer.errors, expected) +# Test for issue #467 +class FieldLabelTest(TestCase): + def setUp(self): + self.serializer_class = BasicModelSerializer + + def test_label_from_model(self): + """ + Validates that label and help_text are correctly copied from the model class. + """ + serializer = self.serializer_class() + text_field = serializer.fields['text'] + + self.assertEquals('Text comes here', text_field.label) + self.assertEquals('Text description.', text_field.help_text) + + def test_field_ctor(self): + """ + This is check that ctor supports both label and help_text. + """ + self.assertEquals('Label', fields.Field(label='Label', help_text='Help').label) + self.assertEquals('Help', fields.CharField(label='Label', help_text='Help').help_text) + self.assertEquals('Label', relations.ManyHyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help').label) + + class AttributeMappingOnAutogeneratedFieldsTests(TestCase): def setUp(self): @@ -1402,3 +1427,4 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase): def test_url_field(self): self.field_test('url_field') +