diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 43950c4bf..5fbcf700f 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -11,6 +11,7 @@ from django.http import Http404 from rest_framework import status from rest_framework.response import Response from rest_framework.request import clone_request +from rest_framework.settings import api_settings import warnings @@ -60,7 +61,7 @@ class CreateModelMixin(object): def get_success_headers(self, data): try: - return {'Location': data['url']} + return {'Location': data[api_settings.URL_FIELD_NAME]} except (TypeError, KeyError): return {} diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 02185c2ff..e779d0d1d 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -332,6 +332,7 @@ class HyperlinkedRelatedField(RelatedField): self.lookup_field = kwargs.pop('lookup_field', self.lookup_field) self.format = kwargs.pop('format', None) + self.force_absolute = kwargs.pop('absolute', False) # These are pending deprecation if 'pk_url_kwarg' in kwargs: @@ -361,7 +362,10 @@ class HyperlinkedRelatedField(RelatedField): lookup_field = getattr(obj, self.lookup_field) kwargs = {self.lookup_field: lookup_field} try: - return reverse(view_name, kwargs=kwargs, request=request, format=format) + return reverse( + view_name, kwargs=kwargs, request=request, + format=format, force_absolute=self.force_absolute + ) except NoReverseMatch: pass @@ -371,7 +375,10 @@ class HyperlinkedRelatedField(RelatedField): pk = obj.pk kwargs = {self.pk_url_kwarg: pk} try: - return reverse(view_name, kwargs=kwargs, request=request, format=format) + return reverse( + view_name, kwargs=kwargs, request=request, + format=format, force_absolute=self.force_absolute + ) except NoReverseMatch: pass @@ -380,7 +387,10 @@ class HyperlinkedRelatedField(RelatedField): # Only try slug if it corresponds to an attribute on the object. kwargs = {self.slug_url_kwarg: slug} try: - ret = reverse(view_name, kwargs=kwargs, request=request, format=format) + ret = reverse( + view_name, kwargs=kwargs, request=request, + format=format, force_absolute=self.force_absolute + ) if self.slug_field == 'slug' and self.slug_url_kwarg == 'slug': # If the lookup succeeds using the default slug params, # then `slug_field` is being used implicitly, and we @@ -503,6 +513,7 @@ class HyperlinkedIdentityField(Field): self.format = kwargs.pop('format', None) lookup_field = kwargs.pop('lookup_field', None) self.lookup_field = lookup_field or self.lookup_field + self.force_absolute = kwargs.pop('force_absolute', False) # These are pending deprecation if 'pk_url_kwarg' in kwargs: @@ -572,7 +583,10 @@ class HyperlinkedIdentityField(Field): return None try: - return reverse(view_name, kwargs=kwargs, request=request, format=format) + return reverse( + view_name, kwargs=kwargs, request=request, + format=format, force_absolute=self.force_absolute + ) except NoReverseMatch: pass @@ -581,7 +595,10 @@ class HyperlinkedIdentityField(Field): # Otherwise, the default `lookup_field = 'pk'` has us covered. kwargs = {self.pk_url_kwarg: obj.pk} try: - return reverse(view_name, kwargs=kwargs, request=request, format=format) + return reverse( + view_name, kwargs=kwargs, request=request, + format=format, force_absolute=self.force_absolute + ) except NoReverseMatch: pass @@ -590,7 +607,10 @@ class HyperlinkedIdentityField(Field): # Only use slug lookup if a slug field exists on the model kwargs = {self.slug_url_kwarg: slug} try: - return reverse(view_name, kwargs=kwargs, request=request, format=format) + return reverse( + view_name, kwargs=kwargs, request=request, + format=format, force_absolute=self.force_absolute + ) except NoReverseMatch: pass diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py index a51b07f54..93fdce92e 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -4,9 +4,10 @@ Provide reverse functions that return fully qualified URLs from __future__ import unicode_literals from django.core.urlresolvers import reverse as django_reverse from django.utils.functional import lazy +from rest_framework.settings import api_settings -def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra): +def reverse(viewname, args=None, kwargs=None, request=None, format=None, force_absolute=False, **extra): """ Same as `django.core.urlresolvers.reverse`, but optionally takes a request and returns a fully qualified URL, using the request to get the base URL. @@ -15,6 +16,9 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra kwargs = kwargs or {} kwargs['format'] = format url = django_reverse(viewname, args=args, kwargs=kwargs, **extra) + + if api_settings.RELATIVE_URLS and not force_absolute: + return url if request: return request.build_absolute_uri(url) return url diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b22ca5783..1726746de 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -990,6 +990,7 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): super(HyperlinkedModelSerializerOptions, self).__init__(meta) self.view_name = getattr(meta, 'view_name', None) self.lookup_field = getattr(meta, 'lookup_field', None) + self.url_field_name = getattr(meta, 'url_field_name', api_settings.URL_FIELD_NAME) class HyperlinkedModelSerializer(ModelSerializer): @@ -1008,13 +1009,13 @@ class HyperlinkedModelSerializer(ModelSerializer): if self.opts.view_name is None: self.opts.view_name = self._get_default_view_name(self.opts.model) - if 'url' not in fields: + if self.opts.url_field_name not in fields: url_field = self._hyperlink_identify_field_class( view_name=self.opts.view_name, lookup_field=self.opts.lookup_field ) ret = self._dict_class() - ret['url'] = url_field + ret[self.opts.url_field_name] = url_field ret.update(fields) fields = ret @@ -1050,7 +1051,7 @@ class HyperlinkedModelSerializer(ModelSerializer): We need to override the default, to use the url as the identity. """ try: - return data.get('url', None) + return data.get(self.opts.url_field_name, None) except AttributeError: return None diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 8abaf1409..68c9d358c 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -95,6 +95,8 @@ DEFAULTS = { 'URL_FORMAT_OVERRIDE': 'format', 'FORMAT_SUFFIX_KWARG': 'format', + 'URL_FIELD_NAME': 'url', + 'RELATIVE_URLS': False, # Input and output formats 'DATE_INPUT_FORMATS': ( diff --git a/rest_framework/tests/test_hyperlinkedserializers.py b/rest_framework/tests/test_hyperlinkedserializers.py index 61e613d75..5a0df6a43 100644 --- a/rest_framework/tests/test_hyperlinkedserializers.py +++ b/rest_framework/tests/test_hyperlinkedserializers.py @@ -3,6 +3,7 @@ import json from django.test import TestCase from rest_framework import generics, status, serializers from rest_framework.compat import patterns, url +from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory from rest_framework.tests.models import ( Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, @@ -331,3 +332,51 @@ class TestOverriddenURLField(TestCase): serializer.data, {'title': 'New blog post', 'url': 'foo bar'} ) + + +class TestGlobalURLOverrides(TestCase): + urls = 'rest_framework.tests.test_hyperlinkedserializers' + + def setUp(self): + self.old_url_fname = api_settings.URL_FIELD_NAME + self.old_relative_urls = api_settings.RELATIVE_URLS + api_settings.URL_FIELD_NAME = 'global_url_field' + api_settings.RELATIVE_URLS = True + + class StandardSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = BlogPost + fields = ('title', 'global_url_field') + self.Serializer = StandardSerializer + self.obj = BlogPost.objects.create(title="New blog post") + self.context = {'request': factory.get('/basic/')} + + def tearDown(self): + api_settings.URL_FIELD_NAME = self.old_url_fname + api_settings.RELATIVE_URLS = self.old_relative_urls + + def test_serializer_overridden_url_field_name(self): + """ + The url field name should respect overriding at the serializer level. + """ + class URLFieldNameSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = BlogPost + fields = ('title', 'serializer_url_field') + url_field_name = "serializer_url_field" + serializer = URLFieldNameSerializer(self.obj, context=self.context) + self.assertIn('serializer_url_field', serializer.data) + + def test_globally_overridden_url_field_name(self): + """ + The url field name should respect overriding for all serializers. + """ + serializer = self.Serializer(self.obj, context=self.context) + self.assertIn('global_url_field', serializer.data) + + def test_relative_urls(self): + """ + Test whether url fields can be made relative across the board. + """ + serializer = self.Serializer(self.obj, context=self.context) + self.assertTrue(serializer.data['global_url_field'].startswith('/'))