From 66b949627d71c4c8e1b41f2d34ae610505832102 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Thu, 16 Feb 2012 11:03:05 +0900 Subject: [PATCH 01/12] Create a custom reverse() function (not implemented yet) --- djangorestframework/urlresolvers.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 djangorestframework/urlresolvers.py diff --git a/djangorestframework/urlresolvers.py b/djangorestframework/urlresolvers.py new file mode 100644 index 000000000..0fd88ec1a --- /dev/null +++ b/djangorestframework/urlresolvers.py @@ -0,0 +1,4 @@ +from django.core.urlresolvers import reverse + +def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): + raise NotImplementedError From 4655e3e3d9d2867174caf332f1d5e49cb068c186 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Thu, 16 Feb 2012 11:03:42 +0900 Subject: [PATCH 02/12] Test the custom reverse() function instead of the one provided by Django --- djangorestframework/tests/reverse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py index c49caca0d..35f697323 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -1,8 +1,8 @@ from django.conf.urls.defaults import patterns, url -from django.core.urlresolvers import reverse from django.test import TestCase from django.utils import simplejson as json +from djangorestframework.urlresolvers import reverse from djangorestframework.views import View from djangorestframework.response import Response From 175032e3b4dfa3954936b27cfd591deb6406f890 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 18:05:12 +0900 Subject: [PATCH 03/12] Move the new `reverse' function from urlresolvers to utils --- djangorestframework/tests/reverse.py | 2 +- djangorestframework/urlresolvers.py | 4 ---- djangorestframework/utils/__init__.py | 5 ++++- 3 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 djangorestframework/urlresolvers.py diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py index 35f697323..71e97f4b9 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -2,7 +2,7 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.utils import simplejson as json -from djangorestframework.urlresolvers import reverse +from djangorestframework.utils import reverse from djangorestframework.views import View from djangorestframework.response import Response diff --git a/djangorestframework/urlresolvers.py b/djangorestframework/urlresolvers.py deleted file mode 100644 index 0fd88ec1a..000000000 --- a/djangorestframework/urlresolvers.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.core.urlresolvers import reverse - -def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): - raise NotImplementedError diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index fbe55474f..817243049 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -1,6 +1,6 @@ from django.utils.encoding import smart_unicode from django.utils.xmlutils import SimplerXMLGenerator -from django.core.urlresolvers import resolve +from django.core.urlresolvers import resolve, reverse from django.conf import settings from djangorestframework.compat import StringIO @@ -180,3 +180,6 @@ class XMLRenderer(): def dict2xml(input): return XMLRenderer().dict2xml(input) + +def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): + raise NotImplementedError From 4e11d7d0418cd6fda881df8dc4541d8c674e5db2 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 18:33:45 +0900 Subject: [PATCH 04/12] Stop using set_script_prefix --- djangorestframework/views.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 95fa119d7..c8e832bce 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -188,12 +188,6 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): Required if you want to do things like set `request.upload_handlers` before the authentication and dispatch handling is run. """ - # Calls to 'reverse' will not be fully qualified unless we set the - # scheme/host/port here. - self.orig_prefix = get_script_prefix() - if not (self.orig_prefix.startswith('http:') or self.orig_prefix.startswith('https:')): - prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host()) - set_script_prefix(prefix + self.orig_prefix) return request def final(self, request, response, *args, **kargs): @@ -201,9 +195,6 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): Returns an `HttpResponse`. This method is a hook for any code that needs to run after everything else in the view. """ - # Restore script_prefix. - set_script_prefix(self.orig_prefix) - # Always add these headers. response['Allow'] = ', '.join(allowed_methods(self)) # sample to allow caching using Vary http header From afb2c01cf3e92f0ca7494c6e97273bddaea270c8 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 18:34:31 +0900 Subject: [PATCH 05/12] Pass the request object to `reverse' in the reverse tests --- djangorestframework/tests/reverse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py index 71e97f4b9..05c21faa8 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -12,7 +12,7 @@ class MockView(View): permissions = () def get(self, request): - return Response(reverse('another')) + return Response(reverse('another', request)) urlpatterns = patterns('', url(r'^$', MockView.as_view()), From 1cac472a572fd20ba999401f3b8d171a1f52b49d Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 18:46:47 +0900 Subject: [PATCH 06/12] Implement a custom reverse that builds absolute URLs with a request object --- djangorestframework/utils/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index 817243049..67882a37e 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -1,6 +1,6 @@ from django.utils.encoding import smart_unicode from django.utils.xmlutils import SimplerXMLGenerator -from django.core.urlresolvers import resolve, reverse +from django.core.urlresolvers import resolve, reverse as django_reverse from django.conf import settings from djangorestframework.compat import StringIO @@ -181,5 +181,6 @@ class XMLRenderer(): def dict2xml(input): return XMLRenderer().dict2xml(input) -def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): - raise NotImplementedError + +def reverse(viewname, request, *args, **kwargs): + return request.build_absolute_uri(django_reverse(viewname, *args, **kwargs)) From fbf03c78d84eeec9792ae2abcda889c36572e1ea Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 18:58:42 +0900 Subject: [PATCH 07/12] Update resources.py to use the new custom `reverse()' --- djangorestframework/resources.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py index 15b3579de..eadc11d08 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -1,10 +1,10 @@ from django import forms -from django.core.urlresolvers import reverse, get_urlconf, get_resolver, NoReverseMatch +from django.core.urlresolvers import get_urlconf, get_resolver, NoReverseMatch from django.db import models from djangorestframework.response import ImmediateResponse from djangorestframework.serializer import Serializer, _SkipField -from djangorestframework.utils import as_tuple +from djangorestframework.utils import as_tuple, reverse class BaseResource(Serializer): @@ -354,7 +354,7 @@ class ModelResource(FormResource): instance_attrs[param] = attr try: - return reverse(self.view_callable[0], kwargs=instance_attrs) + return reverse(self.view_callable[0], self.view.request, kwargs=instance_attrs) except NoReverseMatch: pass raise _SkipField From 8bc2fc54c5990ce827d057363bb14b8bca360dbd Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 19:28:50 +0900 Subject: [PATCH 08/12] Update examples to use the new custom `reverse()' This fixes #167 except for the docs --- examples/blogpost/resources.py | 6 +++--- examples/blogpost/tests.py | 3 +-- examples/mixin/urls.py | 4 ++-- examples/objectstore/views.py | 6 +++--- examples/permissionsexample/views.py | 6 +++--- examples/pygments_api/views.py | 6 +++--- examples/requestexample/views.py | 4 ++-- examples/resourceexample/views.py | 5 ++--- examples/sandbox/views.py | 18 +++++++++--------- 9 files changed, 28 insertions(+), 30 deletions(-) diff --git a/examples/blogpost/resources.py b/examples/blogpost/resources.py index 5a3c1ce2b..d4e0594d8 100644 --- a/examples/blogpost/resources.py +++ b/examples/blogpost/resources.py @@ -1,5 +1,5 @@ -from django.core.urlresolvers import reverse from djangorestframework.resources import ModelResource +from djangorestframework.utils import reverse from blogpost.models import BlogPost, Comment @@ -12,7 +12,7 @@ class BlogPostResource(ModelResource): ordering = ('-created',) def comments(self, instance): - return reverse('comments', kwargs={'blogpost': instance.key}) + return reverse('comments', request, kwargs={'blogpost': instance.key}) class CommentResource(ModelResource): @@ -24,4 +24,4 @@ class CommentResource(ModelResource): ordering = ('-created',) def blogpost(self, instance): - return reverse('blog-post', kwargs={'key': instance.blogpost.key}) + return reverse('blog-post', request, kwargs={'key': instance.blogpost.key}) diff --git a/examples/blogpost/tests.py b/examples/blogpost/tests.py index 5aa4f89f4..9f72e6862 100644 --- a/examples/blogpost/tests.py +++ b/examples/blogpost/tests.py @@ -1,12 +1,11 @@ """Test a range of REST API usage of the example application. """ -from django.core.urlresolvers import reverse from django.test import TestCase -from django.core.urlresolvers import reverse from django.utils import simplejson as json from djangorestframework.compat import RequestFactory +from djangorestframework.utils import reverse from djangorestframework.views import InstanceModelView, ListOrCreateModelView from blogpost import models, urls diff --git a/examples/mixin/urls.py b/examples/mixin/urls.py index 58cf370c1..c899467b2 100644 --- a/examples/mixin/urls.py +++ b/examples/mixin/urls.py @@ -2,9 +2,9 @@ from djangorestframework.compat import View # Use Django 1.3's django.views.gen from djangorestframework.mixins import ResponseMixin from djangorestframework.renderers import DEFAULT_RENDERERS from djangorestframework.response import Response +from djangorestframework.utils import reverse from django.conf.urls.defaults import patterns, url -from django.core.urlresolvers import reverse class ExampleView(ResponseMixin, View): @@ -14,7 +14,7 @@ class ExampleView(ResponseMixin, View): def get(self, request): response = Response({'description': 'Some example content', - 'url': reverse('mixin-view')}, status=200) + 'url': reverse('mixin-view', request)}, status=200) self.response = self.prepare_response(response) return self.response diff --git a/examples/objectstore/views.py b/examples/objectstore/views.py index ae5453948..370383d0a 100644 --- a/examples/objectstore/views.py +++ b/examples/objectstore/views.py @@ -1,6 +1,6 @@ from django.conf import settings -from django.core.urlresolvers import reverse +from djangorestframework.utils import reverse from djangorestframework.views import View from djangorestframework.response import Response from djangorestframework import status @@ -41,7 +41,7 @@ class ObjectStoreRoot(View): filepaths = [os.path.join(OBJECT_STORE_DIR, file) for file in os.listdir(OBJECT_STORE_DIR) if not file.startswith('.')] ctime_sorted_basenames = [item[0] for item in sorted([(os.path.basename(path), os.path.getctime(path)) for path in filepaths], key=operator.itemgetter(1), reverse=True)] - return Response([reverse('stored-object', kwargs={'key':key}) for key in ctime_sorted_basenames]) + return Response([reverse('stored-object', request, kwargs={'key':key}) for key in ctime_sorted_basenames]) def post(self, request): """ @@ -51,7 +51,7 @@ class ObjectStoreRoot(View): pathname = os.path.join(OBJECT_STORE_DIR, key) pickle.dump(self.CONTENT, open(pathname, 'wb')) remove_oldest_files(OBJECT_STORE_DIR, MAX_FILES) - self.headers['Location'] = reverse('stored-object', kwargs={'key':key}) + self.headers['Location'] = reverse('stored-object', request, kwargs={'key':key}) return Response(self.CONTENT, status=status.HTTP_201_CREATED) diff --git a/examples/permissionsexample/views.py b/examples/permissionsexample/views.py index bcf6619cf..0bc31b270 100644 --- a/examples/permissionsexample/views.py +++ b/examples/permissionsexample/views.py @@ -1,7 +1,7 @@ from djangorestframework.views import View from djangorestframework.response import Response from djangorestframework.permissions import PerUserThrottling, IsAuthenticated -from django.core.urlresolvers import reverse +from djangorestframework.utils import reverse class PermissionsExampleView(View): @@ -13,11 +13,11 @@ class PermissionsExampleView(View): return Response([ { 'name': 'Throttling Example', - 'url': reverse('throttled-resource') + 'url': reverse('throttled-resource', request) }, { 'name': 'Logged in example', - 'url': reverse('loggedin-resource') + 'url': reverse('loggedin-resource', request) }, ]) diff --git a/examples/pygments_api/views.py b/examples/pygments_api/views.py index 852b67309..25e01fe42 100644 --- a/examples/pygments_api/views.py +++ b/examples/pygments_api/views.py @@ -1,10 +1,10 @@ from __future__ import with_statement # for python 2.5 from django.conf import settings -from django.core.urlresolvers import reverse from djangorestframework.resources import FormResource from djangorestframework.response import Response from djangorestframework.renderers import BaseRenderer +from djangorestframework.utils import reverse from djangorestframework.views import View from djangorestframework import status @@ -61,7 +61,7 @@ class PygmentsRoot(View): Return a list of all currently existing snippets. """ unique_ids = [os.path.split(f)[1] for f in list_dir_sorted_by_ctime(HIGHLIGHTED_CODE_DIR)] - return Response([reverse('pygments-instance', args=[unique_id]) for unique_id in unique_ids]) + return Response([reverse('pygments-instance', request, args=[unique_id]) for unique_id in unique_ids]) def post(self, request): """ @@ -81,7 +81,7 @@ class PygmentsRoot(View): remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES) - self.headers['Location'] = reverse('pygments-instance', args=[unique_id]) + self.headers['Location'] = reverse('pygments-instance', request, args=[unique_id]) return Response(status=status.HTTP_201_CREATED) diff --git a/examples/requestexample/views.py b/examples/requestexample/views.py index b5d2c1e73..c026ad385 100644 --- a/examples/requestexample/views.py +++ b/examples/requestexample/views.py @@ -1,8 +1,8 @@ from djangorestframework.compat import View from django.http import HttpResponse -from django.core.urlresolvers import reverse from djangorestframework.mixins import RequestMixin +from djangorestframework.utils import reverse from djangorestframework.views import View as DRFView from djangorestframework import parsers from djangorestframework.response import Response @@ -14,7 +14,7 @@ class RequestExampleView(DRFView): """ def get(self, request): - return Response([{'name': 'request.DATA Example', 'url': reverse('request-content')},]) + return Response([{'name': 'request.DATA Example', 'url': reverse('request-content', request)},]) class MyBaseViewUsingEnhancedRequest(RequestMixin, View): diff --git a/examples/resourceexample/views.py b/examples/resourceexample/views.py index 44c4176a1..4d3ece640 100644 --- a/examples/resourceexample/views.py +++ b/examples/resourceexample/views.py @@ -1,5 +1,4 @@ -from django.core.urlresolvers import reverse - +from djangorestframework.utils import reverse from djangorestframework.views import View from djangorestframework.response import Response from djangorestframework import status @@ -16,7 +15,7 @@ class ExampleView(View): """ Handle GET requests, returning a list of URLs pointing to 3 other views. """ - return Response({"Some other resources": [reverse('another-example', kwargs={'num':num}) for num in range(3)]}) + return Response({"Some other resources": [reverse('another-example', request, kwargs={'num':num}) for num in range(3)]}) class AnotherExampleView(View): diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py index 49b59b40f..1cc07f23b 100644 --- a/examples/sandbox/views.py +++ b/examples/sandbox/views.py @@ -1,6 +1,6 @@ """The root view for the examples provided with Django REST framework""" -from django.core.urlresolvers import reverse +from djangorestframework.utils import reverse from djangorestframework.views import View from djangorestframework.response import Response @@ -29,12 +29,12 @@ class Sandbox(View): Please feel free to browse, create, edit and delete the resources in these examples.""" def get(self, request): - return Response([{'name': 'Simple Resource example', 'url': reverse('example-resource')}, - {'name': 'Simple ModelResource example', 'url': reverse('model-resource-root')}, - {'name': 'Simple Mixin-only example', 'url': reverse('mixin-view')}, - {'name': 'Object store API', 'url': reverse('object-store-root')}, - {'name': 'Code highlighting API', 'url': reverse('pygments-root')}, - {'name': 'Blog posts API', 'url': reverse('blog-posts-root')}, - {'name': 'Permissions example', 'url': reverse('permissions-example')}, - {'name': 'Simple request mixin example', 'url': reverse('request-example')} + return Response([{'name': 'Simple Resource example', 'url': reverse('example-resource', request)}, + {'name': 'Simple ModelResource example', 'url': reverse('model-resource-root', request)}, + {'name': 'Simple Mixin-only example', 'url': reverse('mixin-view', request)}, + {'name': 'Object store API', 'url': reverse('object-store-root', request)}, + {'name': 'Code highlighting API', 'url': reverse('pygments-root', request)}, + {'name': 'Blog posts API', 'url': reverse('blog-posts-root', request)}, + {'name': 'Permissions example', 'url': reverse('permissions-example', request)}, + {'name': 'Simple request mixin example', 'url': reverse('request-example', request)} ]) From 06ee48c430d9793dbf3d0e3bc6d9fbe006a97806 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Tue, 21 Feb 2012 15:23:31 +0900 Subject: [PATCH 09/12] Add a HOWTO page for the custom `reverse()' --- docs/howto/reverse.rst | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/howto/reverse.rst diff --git a/docs/howto/reverse.rst b/docs/howto/reverse.rst new file mode 100644 index 000000000..e4efbbcab --- /dev/null +++ b/docs/howto/reverse.rst @@ -0,0 +1,47 @@ +Returning URIs from your Web APIs +================================= + + "The central feature that distinguishes the REST architectural style from + other network-based styles is its emphasis on a uniform interface between + components." + + -- Roy Fielding, Architectural Styles and the Design of Network-based Software Architectures + +As a rule, it's probably better practice to return absolute URIs from you web APIs, e.g. "http://example.com/foobar", rather than returning relative URIs, e.g. "/foobar". + +The advantages of doing so are: + +* It's more explicit. +* It leaves less work for your API clients. +* There's no ambiguity about the meaning of the string when it's found in representations such as JSON that do not have a native URI type. +* It allows us to easily do things like markup HTML representations with hyperlinks. + +Django REST framework provides two utility functions to make it simpler to return absolute URIs from your Web API. + +There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink its output for you, which makes browsing the API much easier. + +reverse(viewname, request, ...) +------------------------------- + +The :py:func:`~utils.reverse` function has the same behavior as :py:func:`django.core.urlresolvers.reverse` [1]_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port:: + + from djangorestframework.utils import reverse + from djangorestframework.views import View + + class MyView(View): + def get(self, request): + context = { + 'url': reverse('year-summary', request, args=[1945]) + } + + return Response(context) + +reverse_lazy(viewname, request, ...) +------------------------------------ + +The :py:func:`~utils.reverse_lazy` function has the same behavior as :py:func:`django.core.urlresolvers.reverse_lazy` [2]_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port. + +.. rubric:: Footnotes + +.. [1] https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse +.. [2] https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse-lazy From dbd19a96a8f5afbe713a047f1214fd7f020177b8 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Tue, 21 Feb 2012 15:29:47 +0900 Subject: [PATCH 10/12] Include the `utils' module in the docs --- docs/library/utils.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/library/utils.rst diff --git a/docs/library/utils.rst b/docs/library/utils.rst new file mode 100644 index 000000000..653f24fde --- /dev/null +++ b/docs/library/utils.rst @@ -0,0 +1,5 @@ +:mod:`utils` +============== + +.. automodule:: utils + :members: From e5b6711afd1791902cfbb3f33054b6919b78dc98 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Tue, 21 Feb 2012 15:30:00 +0900 Subject: [PATCH 11/12] Add a docstring for `reverse()' --- djangorestframework/utils/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index 67882a37e..13b3c9b6d 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -183,4 +183,8 @@ def dict2xml(input): def reverse(viewname, request, *args, **kwargs): + """ + Do the same as :py:func:`django.core.urlresolvers.reverse` but using + *request* to build a fully qualified URL. + """ return request.build_absolute_uri(django_reverse(viewname, *args, **kwargs)) From ee377fe2f02023b4726a6dd614104fd895e5e776 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Tue, 21 Feb 2012 15:55:39 +0900 Subject: [PATCH 12/12] Implement `reverse_lazy()' --- djangorestframework/utils/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index 13b3c9b6d..afef4f195 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -1,3 +1,4 @@ +import django from django.utils.encoding import smart_unicode from django.utils.xmlutils import SimplerXMLGenerator from django.core.urlresolvers import resolve, reverse as django_reverse @@ -188,3 +189,13 @@ def reverse(viewname, request, *args, **kwargs): *request* to build a fully qualified URL. """ return request.build_absolute_uri(django_reverse(viewname, *args, **kwargs)) + +if django.VERSION >= (1, 4): + from django.core.urlresolvers import reverse_lazy as django_reverse_lazy + + def reverse_lazy(viewname, request, *args, **kwargs): + """ + Do the same as :py:func:`django.core.urlresolvers.reverse_lazy` but using + *request* to build a fully qualified URL. + """ + return request.build_absolute_uri(django_reverse_lazy(viewname, *args, **kwargs))