Repository: encode/django-rest-framework

Issue:
`get_name()` call for hyperlinked relationships not required for JSON - should be lazy.
## Checklist
- [x] I have verified that that issue exists against the `master` branch of Django REST framework.
- [x] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
- [x] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.)
- [x] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](http://www.django-rest-framework.org/topics/third-party-resources/#about-third-party-packages) where possible.)
- [x] I have reduced the issue to the simplest possible case.
- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)
## Steps to reproduce
1. Set up a model with a related model.
2. Use that related model in the `__str__` method of the model.
3. Set up a viewset / HyperlinkedModelSerializer / etc. for the model with no nesting and no select_related() call.
## Expected behavior

The API should run one query to fetch the data to be serialized to JSON.
## Actual behavior

The API runs one query to fetch the data to be serialized. And then it runs N queries by calling get_name on each object (which calls `__str__`, which references the related model).

I suppose you could say that this is my fault for not having a 100% pure `__str__` method - but sometimes practicality beats purity. `__str__` for me is primarily supposed to make it easy for me to tell what an object is when introspecting it in the shell or some other situation. I would also expect it to be called, say, when I'm in the admin looking at a list of objects and their representations.

I would _not_ expect it to be called when I'm looking at a JSON serialized version of the model that doesn't even use the results of the method anywhere. This seems like a pretty big potential pitfall that people might not even realize they're walking into.

Potential solutions:
- make it lazy somehow?
- don't call it if the format is JSON?



Discussion:
> and no select_related() call.

Isn't the danger mitigated when you would use `select_related` on your serializer? 

This could be another motivation to get https://github.com/tomchristie/django-rest-framework/issues/1977 out.

Good point, yup, we return `Hyperlink(url, name)`, rather than the raw url, so as you say a `__str__` on the model that requires a lookup will be a problem.

We should make the call lazy.

Retitled to better match the intent.


Resolving patch:
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
--- 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 @@ def get_url(self, obj, view_name, request, format):
         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 @@ def to_representation(self, value):
         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
--- 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('<a href=%s>%s</a>' % (value, escape(value.name)))
+        name = six.text_type(value.obj)
+        return mark_safe('<a href=%s>%s</a>' % (value, escape(name)))
     if value is None or isinstance(value, bool):
         return mark_safe('<code>%s</code>' % {True: 'true', False: 'false', None: 'null'}[value])
     elif isinstance(value, list):


Test patch:
diff --git a/tests/test_lazy_hyperlinks.py b/tests/test_lazy_hyperlinks.py
new file mode 100644
--- /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<pk>[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 == '<a href=/example/1/>An example</a>'
+        assert str_called