diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 50a09701e..80c7a859f 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -322,6 +322,7 @@ By default, `HyperlinkedRelatedField` is read-write, although you can change thi * `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`. * `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`. * `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships. +* `use_absolute_urls` - This option will overwrite the serializer option `use_absolute_urls`. ## HyperLinkedIdentityField diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 19efde3c7..5f7839f4c 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -189,6 +189,14 @@ As an example, let's create a field that can be used represent the class name of """ pass +## Meta options + +**TODO** add remaining serializer meta options + +### `use_absolute_urls` + +This setting will overwrite the `REST_FRAMEWORK` option `USE_ABSOLUTE_URLS` and can also be overwritten by the field attribute `use_absolute_urls`. + --- # ModelSerializers diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 7884d096b..a9ab12319 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -166,4 +166,10 @@ Default: `'format'` Default: `'format'` +## USE_ABSOLUTE_URLS + +Default: `True` + +This setting can overwritten by the serializer meta option `use_absolute_urls` which also can be overwritten by the field attribute `use_absolute_urls`. + [cite]: http://www.python.org/dev/peps/pep-0020/ diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 4f83cfd8f..c6715df6d 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -4,6 +4,11 @@ > > — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. +## Master + +* Added support for absolute/relative url switching +* Bugfix: Fix absolute/relative url mix + ## 2.1.9 **Date**: 11th Dec 2012 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index da588082c..313a4c113 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -543,8 +543,14 @@ class HyperlinkedRelatedField(RelatedField): self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) self.format = kwargs.pop('format', None) + + self.use_absolute_urls = kwargs.pop('use_absolute_urls', None) super(HyperlinkedRelatedField, self).__init__(*args, **kwargs) + def initialize(self, parent, field_name): + super(HyperlinkedRelatedField, self).initialize(parent, field_name) + self.use_absolute_urls = self.use_absolute_urls or self.parent.use_absolute_urls + def get_slug_field(self): """ Get the name of a slug field to be used to look up by slug. @@ -555,12 +561,13 @@ class HyperlinkedRelatedField(RelatedField): view_name = self.view_name request = self.context.get('request', None) format = self.format or self.context.get('format', None) + use_absolute_urls = self.use_absolute_urls pk = getattr(obj, 'pk', None) if pk is None: return 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, use_absolute_urls=use_absolute_urls) except: pass @@ -571,13 +578,13 @@ class HyperlinkedRelatedField(RelatedField): kwargs = {self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + return reverse(self.view_name, kwargs=kwargs, request=request, format=format, use_absolute_urls=use_absolute_urls) except: pass kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + return reverse(self.view_name, kwargs=kwargs, request=request, format=format, use_absolute_urls=use_absolute_urls) except: pass @@ -651,15 +658,21 @@ class HyperlinkedIdentityField(Field): self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) + self.use_absolute_urls = kwargs.pop('use_absolute_urls', None) super(HyperlinkedIdentityField, self).__init__(*args, **kwargs) + def initialize(self, parent, field_name): + super(HyperlinkedIdentityField, self).initialize(parent, field_name) + self.use_absolute_urls = self.use_absolute_urls or self.parent.use_absolute_urls + def field_to_native(self, obj, field_name): request = self.context.get('request', None) format = self.format or self.context.get('format', None) view_name = self.view_name or self.parent.opts.view_name kwargs = {self.pk_url_kwarg: obj.pk} + use_absolute_urls = self.use_absolute_urls try: - return reverse(view_name, kwargs=kwargs, request=request, format=format) + return reverse(view_name, kwargs=kwargs, request=request, format=format, use_absolute_urls=use_absolute_urls) except: pass @@ -670,13 +683,13 @@ class HyperlinkedIdentityField(Field): kwargs = {self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + return reverse(self.view_name, kwargs=kwargs, request=request, format=format, use_absolute_urls=use_absolute_urls) except: pass kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + return reverse(self.view_name, kwargs=kwargs, request=request, format=format, use_absolute_urls=use_absolute_urls) except: pass diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index d241ade7c..203391515 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -15,7 +15,11 @@ class NextPageField(serializers.Field): return None page = value.next_page_number() request = self.context.get('request') - url = request and request.build_absolute_uri() or '' + if self.parent.use_absolute_urls: + assert request, "request is required for building absolute url" + url = request.build_absolute_uri() + else: + url = request and request.get_full_path() or '' return replace_query_param(url, self.page_field, page) @@ -30,7 +34,11 @@ class PreviousPageField(serializers.Field): return None page = value.previous_page_number() request = self.context.get('request') - url = request and request.build_absolute_uri() or '' + if self.parent.use_absolute_urls: + assert request, "request is required for building absolute url" + url = request.build_absolute_uri() + else: + url = request and request.get_full_path() or '' return replace_query_param(url, self.page_field, page) diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py index c9db02f06..34108fa85 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -3,9 +3,10 @@ Provide reverse functions that return fully qualified URLs """ 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, use_absolute_urls=api_settings.USE_ABSOLUTE_URLS, **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. @@ -14,8 +15,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 request: - return request.build_absolute_uri(url) + if use_absolute_urls: + assert request, "request is required for building absolute url" + url = request.build_absolute_uri(url) return url diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index caa7c980f..b46a5e388 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -16,6 +16,7 @@ from rest_framework.compat import get_concrete_model from rest_framework.fields import * +from rest_framework.settings import api_settings class DictWithMetadata(dict): @@ -91,6 +92,7 @@ class SerializerOptions(object): self.depth = getattr(meta, 'depth', 0) self.fields = getattr(meta, 'fields', ()) self.exclude = getattr(meta, 'exclude', ()) + self.use_absolute_urls = getattr(meta, 'use_absolute_urls', api_settings.USE_ABSOLUTE_URLS) class BaseSerializer(Field): @@ -101,12 +103,13 @@ class BaseSerializer(Field): _dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations. def __init__(self, instance=None, data=None, files=None, - context=None, partial=False, **kwargs): + context=None, partial=False, use_absolute_urls=None, **kwargs): super(BaseSerializer, self).__init__(**kwargs) self.opts = self._options_class(self.Meta) self.parent = None self.root = None self.partial = partial + self.use_absolute_urls = use_absolute_urls if use_absolute_urls is not None else self.opts.use_absolute_urls self.context = context or {} diff --git a/rest_framework/settings.py b/rest_framework/settings.py index ee24a4ad9..c65ee998b 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -74,6 +74,8 @@ DEFAULTS = { 'URL_FORMAT_OVERRIDE': 'format', 'FORMAT_SUFFIX_KWARG': 'format', + + 'USE_ABSOLUTE_URLS': True, } diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index 24bf61bf8..c0ba8ca75 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -26,6 +26,14 @@ class PhotoSerializer(serializers.Serializer): return Photo(**attrs) +class PhotoUrlSerializer(PhotoSerializer): + url = serializers.HyperlinkedIdentityField(view_name='photoswithmixedurls-detail', use_absolute_urls=True) + + class Meta: + model = Photo + use_absolute_urls = False + + class BasicList(generics.ListCreateAPIView): model = BasicModel model_serializer_class = serializers.HyperlinkedModelSerializer @@ -74,6 +82,16 @@ class AlbumDetail(generics.RetrieveAPIView): model = Album +class PhotoUrlList(generics.ListAPIView): + model = Photo + serializer_class = PhotoUrlSerializer + + +class PhotoUrlDetail(generics.RetrieveAPIView): + model = Photo + serializer_class = PhotoUrlSerializer + + class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView): model = OptionalRelationModel model_serializer_class = serializers.HyperlinkedModelSerializer @@ -90,6 +108,8 @@ urlpatterns = patterns('', url(r'^comments/(?P\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'), url(r'^albums/(?P\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'), url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'), + url(r'^photos-with-mixed-urls/$', PhotoUrlList.as_view(), name='photoswithmixedurls-list'), + url(r'^photos-with-mixed-urls/(?P<pk>\d+)/$', PhotoUrlDetail.as_view(), name='photoswithmixedurls-detail'), url(r'^optionalrelation/(?P<pk>\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'), ) @@ -260,3 +280,47 @@ class TestOptionalRelationHyperlinkedView(TestCase): data=json.dumps(self.data), content_type='application/json') self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class TestUrlOptionsView(TestCase): + urls = 'rest_framework.tests.hyperlinkedserializers' + + def setUp(self): + """ + Create a album and photos + """ + self.album = Album.objects.create(title="test-album") + items = ['beach', 'sunset', 'moon'] + for item in items: + Photo(description=item, album=self.album).save() + self.objects = Photo.objects + self.data = [ + { + 'url': 'http://testserver/photos-with-mixed-urls/%d/' % obj.id, + 'description': obj.description, + 'album_url': '/albums/%s/' % obj.album.title + } + for obj in self.objects.all() + ] + self.list_view = PhotoUrlList.as_view() + self.detail_view = PhotoUrlDetail.as_view() + + def test_get_list_view(self): + """ + GET requests to RetrieveAPIView with optional relations should return None + for non existing relations. + """ + request = factory.get('/photos-with-mixed-urls/') + response = self.list_view(request) + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data[:]) + + def test_get_detail_view(self): + """ + GET requests to RetrieveAPIView with optional relations should return None + for non existing relations. + """ + request = factory.get('/photos-with-mixed-urls/1/') + response = self.detail_view(request, pk=1) + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data[0]) \ No newline at end of file diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 3062007d4..2908e1269 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -4,7 +4,7 @@ from django.core.paginator import Paginator from django.test import TestCase from django.test.client import RequestFactory from django.utils import unittest -from rest_framework import generics, status, pagination, filters +from rest_framework import generics, status, pagination, filters, serializers from rest_framework.compat import django_filters from rest_framework.tests.models import BasicModel, FilterableItem @@ -34,6 +34,23 @@ if django_filters: filter_backend = filters.DjangoFilterBackend +class AbsoluteUrlsSerializer(serializers.ModelSerializer): + """ + Serializer for testing absolute urls + """ + class Meta: + use_absolute_urls = True + + +class AbsoluteUrlsView(generics.ListAPIView): + """ + View for testing absolute urls + """ + model = BasicModel + paginate_by = 1 + + + class DefaultPageSizeKwargView(generics.ListAPIView): """ View for testing default paginate_by_param usage @@ -160,19 +177,59 @@ class UnitTestPagination(TestCase): self.last_page = paginator.page(3) def test_native_pagination(self): - serializer = pagination.PaginationSerializer(self.first_page) + serializer = pagination.PaginationSerializer(self.first_page, use_absolute_urls=False) self.assertEquals(serializer.data['count'], 26) self.assertEquals(serializer.data['next'], '?page=2') self.assertEquals(serializer.data['previous'], None) self.assertEquals(serializer.data['results'], self.objects[:10]) - serializer = pagination.PaginationSerializer(self.last_page) + serializer = pagination.PaginationSerializer(self.last_page, use_absolute_urls=False) self.assertEquals(serializer.data['count'], 26) self.assertEquals(serializer.data['next'], None) self.assertEquals(serializer.data['previous'], '?page=2') self.assertEquals(serializer.data['results'], self.objects[20:]) +class TestPaginationWithAbsoluteUrls(TestCase): + """ + Tests for using absolute urls + """ + + def setUp(self): + items = ['foo', 'bar', 'baz'] + for item in items: + BasicModel(text=item).save() + self.objects = BasicModel.objects + self.data = [ + {'id': obj.id, 'text': obj.text} + for obj in self.objects.all() + ] + self.view = AbsoluteUrlsView.as_view() + + def test_paginated_root_view_urls(self): + """ + Tests absolute/relative url switch + """ + request = factory.get('/') + response = self.view(request).render() + self.assertEquals(response.data['count'], 3) + self.assertEquals(response.data['next'], 'http://testserver/?page=2') + self.assertEquals(response.data['previous'], None) + self.assertEquals(response.data['results'], self.data[0:1]) + request = factory.get('/?page=2') + response = self.view(request).render() + self.assertEquals(response.data['count'], 3) + self.assertEquals(response.data['next'], 'http://testserver/?page=3') + self.assertEquals(response.data['previous'], 'http://testserver/?page=1') + self.assertEquals(response.data['results'], self.data[1:2]) + request = factory.get('/?page=3') + response = self.view(request).render() + self.assertEquals(response.data['count'], 3) + self.assertEquals(response.data['next'], None) + self.assertEquals(response.data['previous'], 'http://testserver/?page=2') + self.assertEquals(response.data['results'], self.data[2:3]) + + class TestUnpaginated(TestCase): """ Tests for list views without pagination.