From 66307c5cfd7706c5e5b989febff7ed257e964b19 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Sun, 25 Feb 2018 18:55:47 +0000 Subject: [PATCH 1/2] Router accepts app_name to support hyperlinked relations --- rest_framework/relations.py | 3 + rest_framework/routers.py | 4 +- rest_framework/viewsets.py | 7 ++ tests/test_relations_hyperlink_appname.py | 85 +++++++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tests/test_relations_hyperlink_appname.py diff --git a/rest_framework/relations.py b/rest_framework/relations.py index c87b9299a..686d013b7 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -309,6 +309,9 @@ class HyperlinkedRelatedField(RelatedField): if hasattr(obj, 'pk') and obj.pk in (None, ''): return None + if hasattr(request, 'app_name') and request.app_name is not None: + view_name = request.app_name + ':' + view_name + lookup_value = getattr(obj, self.lookup_field) kwargs = {self.lookup_url_kwarg: lookup_value} return self.reverse(view_name, kwargs=kwargs, request=request, format=format) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 281bbde8a..d614ebcf1 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -147,8 +147,9 @@ class SimpleRouter(BaseRouter): ), ] - def __init__(self, trailing_slash=True): + def __init__(self, trailing_slash=True, app_name=None): self.trailing_slash = '/' if trailing_slash else '' + self.app_name = app_name super(SimpleRouter, self).__init__() def get_default_base_name(self, viewset): @@ -285,6 +286,7 @@ class SimpleRouter(BaseRouter): initkwargs.update({ 'basename': basename, 'detail': route.detail, + 'router': self, }) view = viewset.as_view(mapping, **initkwargs) diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 9a85049bc..c655a616d 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -63,6 +63,9 @@ class ViewSetMixin(object): # value is provided by the router through the initkwargs. cls.basename = None + # Setting a router allows optional resolution of the app_name + cls.router = None + # actions must not be empty if not actions: raise TypeError("The `actions` argument must be provided when " @@ -99,6 +102,9 @@ class ViewSetMixin(object): self.args = args self.kwargs = kwargs + if self.router is not None: + request.app_name = self.router.app_name + # And continue as usual return self.dispatch(request, *args, **kwargs) @@ -115,6 +121,7 @@ class ViewSetMixin(object): view.cls = cls view.initkwargs = initkwargs view.suffix = initkwargs.get('suffix', None) + view.router = initkwargs.get('router', None) view.actions = actions return csrf_exempt(view) diff --git a/tests/test_relations_hyperlink_appname.py b/tests/test_relations_hyperlink_appname.py new file mode 100644 index 000000000..bf59232ad --- /dev/null +++ b/tests/test_relations_hyperlink_appname.py @@ -0,0 +1,85 @@ +from __future__ import unicode_literals + +import pytest +from django.conf.urls import include, url +from django.core.exceptions import ImproperlyConfigured +from django.db import models +from django.test import TestCase + +from rest_framework import routers, serializers, viewsets +from rest_framework.test import APIRequestFactory, URLPatternsTestCase +from rest_framework.utils import json + +factory = APIRequestFactory() +request = factory.get('/') # Just to ensure we have a request in the serializer context + + +class Wine(models.Model): + title = models.CharField(max_length=100) + + +class WineSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Wine + fields = ('url', 'title') + + +class WineViewSet(viewsets.ModelViewSet): + queryset = Wine.objects.all() + serializer_class = WineSerializer + + +router = routers.DefaultRouter() +router.register(r'wines', WineViewSet) + + +class TestHyperlinkedRouterNoName(URLPatternsTestCase, TestCase): + + urlpatterns = [ + url(r'^api/', include(router.urls)), + ] + + def test_no_name_works(self): + w = Wine(title="Shiraz") + w.save() + + response = self.client.get('/api/wines/') + assert response.status_code == 200 + assert json.loads(response.content.decode('utf-8')) == [{'title': 'Shiraz', 'url': 'http://testserver/api/wines/1/'}] + + +# Failing case with Django 2.0 and HyperlinkedModelSerializer +class TestHyperlinkedRouterFailsWithName(URLPatternsTestCase, TestCase): + urlpatterns = [ + url(r'^api2/', include((router.urls, 'appname2'))), + ] + + def test_hyperlink_fails(self): + w = Wine(title="Shiraz") + w.save() + + with pytest.raises( + ImproperlyConfigured, + message='Could not resolve URL for hyperlinked relationship using view ' + 'name "wine-detail". You may have failed to include the related model in ' + 'your API, or incorrectly configured the `lookup_field` attribute on this field.'): + + self.client.get('/api2/wines/') + + +router2 = routers.DefaultRouter(app_name='appname2') +router2.register(r'wines', WineViewSet) + + +class TestHyperlinkedRouterConfigured(URLPatternsTestCase, TestCase): + urlpatterns = [ + url(r'^api2/', include((router2.urls, 'appname2'))), + ] + + def test_regex_url_path_list(self): + w = Wine(title="Shiraz") + w.save() + + response = self.client.get('/api2/wines/') + assert response.status_code == 200 + assert json.loads(response.content.decode('utf-8')) == [{'title': 'Shiraz', 'url': 'http://testserver/api2/wines/1/'}] From 1d161687a2774b7a814456eda93e2babb7750a05 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Sun, 25 Feb 2018 18:56:21 +0000 Subject: [PATCH 2/2] wip: support for app_name in URLPatternsTestCase --- rest_framework/test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rest_framework/test.py b/rest_framework/test.py index edacf0066..b78332b36 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -385,9 +385,14 @@ class URLPatternsTestCase(testcases.SimpleTestCase): if hasattr(cls._module, 'urlpatterns'): cls._module_urlpatterns = cls._module.urlpatterns + if hasattr(cls._module, 'app_name'): + cls._module_app_name = cls._module.app_name cls._module.urlpatterns = cls.urlpatterns + if hasattr(cls, 'app_name'): + cls._module.app_name = cls.app_name + cls._override.enable() super(URLPatternsTestCase, cls).setUpClass() @@ -400,3 +405,9 @@ class URLPatternsTestCase(testcases.SimpleTestCase): cls._module.urlpatterns = cls._module_urlpatterns else: del cls._module.urlpatterns + + if hasattr(cls, '_module_app_name'): + cls._module.app_name = cls._module_app_name + else: + if hasattr(cls._module, 'app_name'): + del cls._module.app_name