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