diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index f11def6d4..e136a1d43 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -10,6 +10,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 @@ -59,7 +60,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 edaf76d6e..b5f50b7eb 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -335,6 +335,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: @@ -364,7 +365,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 @@ -374,7 +378,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 @@ -383,7 +390,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 @@ -506,6 +516,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: @@ -570,7 +581,10 @@ class HyperlinkedIdentityField(Field): 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 @@ -579,7 +593,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 @@ -588,7 +605,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 d2ddac287..f53f344fd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -893,7 +893,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', 'url') + self.url_field_name = getattr(meta, 'url_field_name', api_settings.URL_FIELD_NAME) class HyperlinkedModelSerializer(ModelSerializer): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index beb511aca..a026b7611 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -81,6 +81,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 129600cb4..65484a228 100644 --- a/rest_framework/tests/test_hyperlinkedserializers.py +++ b/rest_framework/tests/test_hyperlinkedserializers.py @@ -4,6 +4,7 @@ from django.test import TestCase from django.test.client import RequestFactory from rest_framework import generics, status, serializers from rest_framework.compat import patterns, url +from rest_framework.settings import api_settings from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel factory = RequestFactory() @@ -328,3 +329,44 @@ class TestOverriddenURLField(TestCase): serializer.data, {'title': 'New blog post', 'url': 'foo bar'} ) + + +class TestGlobalURLOverrides(TestCase): + def setUp(self): + 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") + + 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) + 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) + import pdb; pdb.set_trace() + print serializer.data + 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) + self.assertTrue(serializer.data['global_url_field'].startswith('/'))