Let's say I have a Django response object.
I want to find the URL (location).
However, the response header does not actually contain a Location or Content-Location field.
How do I determine, from this response object, the URL it is showing?
This is old, but I ran into a similar issue when doing unit tests. Here is how I solved the problem.
You can use the response.redirect_chain and/or the response.request['PATH_INFO'] to grab redirect urls.
Check out the documentation as well! Django Testing Tools: assertRedirects
from django.core.urlresolvers import reverse
from django.test import TestCase
class MyTest(TestCase)
def test_foo(self):
foo_path = reverse('foo')
bar_path = reverse('bar')
data = {'bar': 'baz'}
response = self.client.post(foo_path, data, follow=True)
# Get last redirect
self.assertGreater(len(response.redirect_chain), 0)
# last_url will be something like 'http://testserver/.../'
last_url, status_code = response.redirect_chain[-1]
self.assertIn(bar_path, last_url)
self.assertEqual(status_code, 302)
# Get the exact final path from the response,
# excluding server and get params.
last_path = response.request['PATH_INFO']
self.assertEqual(bar_path, last_path)
# Note that you can also assert for redirects directly.
self.assertRedirects(response, bar_path)
The response does not decide the url, the request does.
The response gives you the content of the response, not the url of it.
Related
I am new to Python and Django. I did a experiment on enforcing request method (e.g. for certain url you can only use GET). Here is my code.
tests.py
from django.test import TestCase, Client
client = Client()
class MyTests(TestCase):
def test_request_method:
""" Sending wrong request methods should result in 405 error """
self.assertEqual(client.post('/mytest', follow = True).status_code, 405)
urls.py
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name = 'index'),
url(r'^mytest/', views.mytest, name = 'mytest'),
]
views.py
from django.http import HttpResponse
def mytest(request):
if request.method == 'GET':
return HttpResponse("Not implemented", status = 500)
else:
return HttpResponse("Only GET method allowed", status = 405)
But the test always returns status 500.
I saw here that this may be related to using follow=True in the client.post() call. However if I use follow=False I will get status 301 instead.
Any ideas? Thank you!
Does it perhaps redirect /mytest to /mytest/? The documentation suggests that by default, a trailing slash is added by doing a redirect if no URL pattern matches without the slash, and to quote:
Note that the redirect may cause any data submitted in a POST request to be lost.
A request caused by commonly used redirect status codes is always a GET request. You could either make the request to /mytest/ or remove the trailing slash from your URL pattern.
I am retrofitting a Django web app for a new client. To this end I have added a url pattern that redirects requests from new client to old url patterns.
from:-
(('api/(?P<phone>\w+)/MessageA', handle_a_message),
('api/(?P<phone>\w+)/MessageB', handle_b_message),
...)
to:-
(('api/(?P<phone>\w+)/MessageA', handle_a_message),
('api/(?P<phone>\w+)/MessageB', handle_b_message),
('api/newclient', handle_newclient)
...)
views.handle_newclient
def handle_newclient(request):
return redirect('/api/%(phone)s/%(msg)s' % request.GET)
This somewhat works. However the new client doesn't do basic auth which those url's need. Also the default output is json where the new client needs plain text. Is there a way I can tweak the headers before redirecting to the existing url's?
Django FBV's should return an HTTPResponse object (or subclass thereof). The Django shorcut redirect returns HttpResponseRedirect which is a subclass of HTTPResponse. This means we can set the headers for redirect() the way we will set headers for a typical HTTPResponse object. We can do that like so:
def my_view(request):
response = redirect('http://www.gamefaqs.com')
# Set 'Test' header and then delete
response['Test'] = 'Test'
del response['Test']
# Set 'Test Header' header
response['Test Header'] = 'Test Header'
return response
Relevant docs here and here.
I try to test view calling by POST. I use follow=True. But test client uses GET method and my POST data are not passed.
here is my view:
def aaa(request):
n = request.method
d = request.POST
template = 'shop/test.html'
return render(request, template, d)
Here is my test:
from django.utils import unittest
from django.test.client import Client
def test_add_to_cart_page(self):
response = self.client.post('/aaa/', {'product': 11}, follow=True)
self.assertEqual(response.status_code, 200)
When the view is called. It is not POST, but GET used and my POST params are empty of course. Can somebody say why its happened?
EDIT:
I made a clean venv with fresh Django. And it works as expected(calls POST) Looks like there is something rotten in the state of Denmark.
follow=True
means that the client follows the redirection.
response = self.client.post('/aaa/', {'product': 11}, follow=True)
means that the response contains the followed response content. There is nothing wrong with your test; it must be doing a POST.
What's weird is that your view doesn't redirect to anything so I don't understand why you use follow=True. Also I don't see why you assume that post isn't working. What's the result of your test?
After investigation I realize that using PREPEND_WWW breaks the test client post requests.
I have been writing tests for one of my django applications and have been looking to get around this problem for quite some time now. I have a view that sends messages using django.contrib.messages for different cases. The view looks something like the following.
from django.contrib import messages
from django.shortcuts import redirect
import custom_messages
def some_view(request):
""" This is a sample view for testing purposes.
"""
some_condition = models.SomeModel.objects.get_or_none(
condition=some_condition)
if some_condition:
messages.success(request, custom_message.SUCCESS)
else:
messages.error(request, custom_message.ERROR)
redirect(some_other_view)
Now, while testing this view client.get's response does not contain the context dictionary that contains the messages as this view uses a redirect. For views that render templates we can get access to the messages list using messages = response.context.get('messages'). How can we get access messages for a view that redirects?
Use the follow=True option in the client.get() call, and the client will follow the redirect. You can then test that the message is in the context of the view you redirected to.
def test_some_view(self):
# use follow=True to follow redirect
response = self.client.get('/some-url/', follow=True)
# don't really need to check status code because assertRedirects will check it
self.assertEqual(response.status_code, 200)
self.assertRedirects(response, '/some-other-url/')
# get message from context and check that expected text is there
message = list(response.context.get('messages'))[0]
self.assertEqual(message.tags, "success")
self.assertTrue("success text" in message.message)
You can use get_messages() with response.wsgi_request like this (tested in Django 1.10):
from django.contrib.messages import get_messages
...
def test_view(self):
response = self.client.get('/some-url/') # you don't need follow=True
self.assertRedirects(response, '/some-other-url/')
# each element is an instance of django.contrib.messages.storage.base.Message
all_messages = [msg for msg in get_messages(response.wsgi_request)]
# here's how you test the first message
self.assertEqual(all_messages[0].tags, "success")
self.assertEqual(all_messages[0].message, "you have done well")
If your views are redirecting and you use follow=true in your request to the test client the above doesn't work. I ended up writing a helper function to get the first (and in my case, only) message sent with the response.
#classmethod
def getmessage(cls, response):
"""Helper method to return message from response """
for c in response.context:
message = [m for m in c.get('messages')][0]
if message:
return message
You include this within your test class and use it like this:
message = self.getmessage(response)
Where response is what you get back from a get or post to a Client.
This is a little fragile but hopefully it saves someone else some time.
I had the same problem when using a 3rd party app.
If you want to get the messages from a view that returns an HttpResponseRedict (from which you can't access the context) from within another view, you can use get_messages(request)
from django.contrib.messages import get_messages
storage = get_messages(request)
for message in storage:
do_something_with_the_message(message)
This clears the message storage though, so if you want to access the messages from a template later on, add:
storage.used = False
Alternative method mocking messages (doesn't need to follow redirect):
from mock import ANY, patch
from django.contrib import messages
#patch('myapp.views.messages.add_message')
def test_some_view(self, mock_add_message):
r = self.client.get('/some-url/')
mock_add_message.assert_called_once_with(ANY, messages.ERROR, 'Expected message.') # or assert_called_with, assert_has_calls...
I just learnt that with Rails is possible to simulate HTTP requests in the console with few lines of code.
Check out: http://37signals.com/svn/posts/3176-three-quick-rails-console-tips (section "Dive into your app").
Is there a similar way to do that with Django? Would be handy.
You can use RequestFactory, which allows
inserting a user into the request
inserting an uploaded file into the request
sending specific parameters to the view
and does not require the additional dependency of using requests.
Note that you have to specify both the URL and the view class, so it takes an extra line of code than using requests.
from django.test import RequestFactory
request_factory = RequestFactory()
my_url = '/my_full/url/here' # Replace with your URL -- or use reverse
my_request = request_factory.get(my_url)
response = MyClasBasedView.as_view()(my_request) # Replace with your view
response.render()
print(response)
To set the user of the request, do something like my_request.user = User.objects.get(id=123) before getting the response.
To send parameters to a class-based view, do something like response = MyClasBasedView.as_view()(my_request, parameter_1, parameter_2)
Extended Example
Here's an example of using RequestFactory with these things in combination
HTTP POST (to url url, functional view view, and a data dictionary post_data)
uploading a single file (path file_path, name file_name, and form field value file_key)
assigning a user to the request (user)
passing on kwargs dictionary from the url (url_kwargs)
SimpleUploadedFile helps format the file in a way that is valid for forms.
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import RequestFactory
request = RequestFactory().post(url, post_data)
with open(file_path, 'rb') as file_ptr:
request.FILES[file_key] = SimpleUploadedFile(file_name, file_ptr.read())
file_ptr.seek(0) # resets the file pointer after the read
if user:
request.user = user
response = view(request, **url_kwargs)
Using RequestFactory from a Python shell
RequestFactory names your server "testserver" by default, which can cause a problem if you're not using it inside test code. You'll see an error like:
DisallowedHost: Invalid HTTP_HOST header: 'testserver'. You may need to add 'testserver' to ALLOWED_HOSTS.
This workaround from #boatcoder's comment shows how to override the default server name to "localhost":
request_factory = RequestFactory(**{"SERVER_NAME": "localhost", "wsgi.url_scheme":"https"}).
How I simulate requests from the python command line is:
Use the excellent requests library
Use the django reverse function
A simple way of simulating requests is:
>>> from django.urls import reverse
>>> import requests
>>> r = requests.get(reverse('app.views.your_view'))
>>> r.text
(prints output)
>>> r.status_code
200
Update: be sure to launch the django shell (via manage.py shell), not a classic python shell.
Update 2: For Django <1.10, change the first line to
from django.core.urlresolvers import reverse
(See tldr; down)
Its an old question,
but just adding an answer, in case someone maybe interested.
Though this might not be the best(or lets say Django) way of doing things.
but you can try doing this way.
Inside your django shell
>>> import requests
>>> r = requests.get('your_full_url_here')
Explanation:
I omitted the reverse(),
explanation being, since reverse() more or less,
finds the url associated to a views.py function,
you may omit the reverse() if you wish to, and put the whole url instead.
For example, if you have a friends app in your django project,
and you want to see the list_all() (in views.py) function in the friends app,
then you may do this.
TLDR;
>>> import requests
>>> url = 'http://localhost:8000/friends/list_all'
>>> r = requests.get(url)