From dcaa1c98dde687bd778e4bc4624094d2af8ed072 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 11 Oct 2016 12:04:56 +0100 Subject: [PATCH] Lazy hyperlink names --- rest_framework/relations.py | 17 ++++--- rest_framework/templatetags/rest_framework.py | 3 +- tests/test_lazy_hyperlinks.py | 49 +++++++++++++++++++ 3 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 tests/test_lazy_hyperlinks.py diff --git a/rest_framework/relations.py b/rest_framework/relations.py index c6eb92a24..c1898d0d8 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -37,14 +37,21 @@ class Hyperlink(six.text_type): We use this for hyperlinked URLs that may render as a named link in some contexts, or render as a plain URL in others. """ - def __new__(self, url, name): + def __new__(self, url, obj): ret = six.text_type.__new__(self, url) - ret.name = name + ret.obj = obj return ret def __getnewargs__(self): return(str(self), self.name,) + @property + def name(self): + # This ensures that we only called `__str__` lazily, + # as in some cases calling __str__ on a model instances *might* + # involve a database lookup. + return six.text_type(self.obj) + is_hyperlink = True @@ -303,9 +310,6 @@ class HyperlinkedRelatedField(RelatedField): kwargs = {self.lookup_url_kwarg: lookup_value} return self.reverse(view_name, kwargs=kwargs, request=request, format=format) - def get_name(self, obj): - return six.text_type(obj) - def to_internal_value(self, data): request = self.context.get('request', None) try: @@ -384,8 +388,7 @@ class HyperlinkedRelatedField(RelatedField): if url is None: return None - name = self.get_name(value) - return Hyperlink(url, name) + return Hyperlink(url, value) class HyperlinkedIdentityField(HyperlinkedRelatedField): diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index c1c8a5396..cfba054f6 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -135,7 +135,8 @@ def add_class(value, css_class): @register.filter def format_value(value): if getattr(value, 'is_hyperlink', False): - return mark_safe('%s' % (value, escape(value.name))) + name = six.text_type(value.obj) + return mark_safe('%s' % (value, escape(name))) if value is None or isinstance(value, bool): return mark_safe('%s' % {True: 'true', False: 'false', None: 'null'}[value]) elif isinstance(value, list): diff --git a/tests/test_lazy_hyperlinks.py b/tests/test_lazy_hyperlinks.py new file mode 100644 index 000000000..cf3ee735f --- /dev/null +++ b/tests/test_lazy_hyperlinks.py @@ -0,0 +1,49 @@ +from django.conf.urls import url +from django.db import models +from django.test import TestCase, override_settings + +from rest_framework import serializers +from rest_framework.renderers import JSONRenderer +from rest_framework.templatetags.rest_framework import format_value + +str_called = False + + +class Example(models.Model): + text = models.CharField(max_length=100) + + def __str__(self): + global str_called + str_called = True + return 'An example' + + +class ExampleSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Example + fields = ('url', 'id', 'text') + + +def dummy_view(request): + pass + + +urlpatterns = [ + url(r'^example/(?P[0-9]+)/$', dummy_view, name='example-detail'), +] + + +@override_settings(ROOT_URLCONF='tests.test_lazy_hyperlinks') +class TestLazyHyperlinkNames(TestCase): + def setUp(self): + self.example = Example.objects.create(text='foo') + + def test_lazy_hyperlink_names(self): + global str_called + context = {'request': None} + serializer = ExampleSerializer(self.example, context=context) + JSONRenderer().render(serializer.data) + assert not str_called + hyperlink_string = format_value(serializer.data['url']) + assert hyperlink_string == 'An example' + assert str_called