So in a DeleteView, the GET request returns a confirmation page, and a simple POST request with no fields except for the csrf_token actually gets the DeleteView to delete the object, upon which the user gets redirected to the success_url.
How can I test this functionality? In my myclass_confirm_delete.html file, I basically have:
<form action="{% url 'remove_myclass' object.id %}" method="post">
{% csrf_token %}
<p>Are you sure you want to remove {{ object.name }}?</p>
<input type="submit" value="Yes" class="btn btn-primary" />
</form>
where {% url 'remove_myclass' object.id %} is the URL of the same exact page. It works in my browser. When I click "Yes," it redirects me to the success page, and the myclass object is removed from the database.
Now I am trying to test this automatically with unit tests. I basically try
response = self.client.get(reverse('remove_myclass', args=(myobject.id,)), follow=True)
self.assertContains(response, 'Are you sure you want to remove') # THIS PART WORKS
self.client.post(reverse('remove_myclass', args=(myobject.id,)), follow=True)
self.assertRedirects(response, reverse('myclass_removed'), status_code=302) # FAILS; status code is 200
If I try print response, I get the same exact response as when I had used the GET request.
It seems like while unit testing, no matter what kind of data I try to send in the POST request, it still gets treated as a GET request...
My class-based view:
class MyclassDelete(DeleteView):
model = myclass
success_url = '/myclass-removed/'
Any ideas?
Yeah, this is because you're forgetting to assign the post request to response, so you're checking the same response twice.
response = self.client.get(reverse('remove_myclass', args=(myobject.id,)), follow=True)
self.assertContains(response, 'Are you sure you want to remove') # THIS PART WORKS
post_response = self.client.post(reverse('remove_myclass', args=(myobject.id,)), follow=True)
self.assertRedirects(post_response, reverse('myclass_removed'), status_code=302)
This should do the trick.
Also, just a tip, trying to assert more than once per unit test is considered bad practice when unit testing. Instead try to break it up so that one test tests the GET and on test tests the POST.
from django.test import TestCase
class TestDifferentRequestMethods(TestCase):
def test_my_get_request(self):
response = self.client.get(reverse('remove_myclass', args=(myobject.id,)), follow=True)
self.assertContains(response, 'Are you sure you want to remove') # THIS PART WORKS
def test_my_post_request(self):
post_response = self.client.post(reverse('remove_myclass', args=(myobject.id,)), follow=True)
self.assertRedirects(post_response, reverse('myclass_removed'), status_code=302)
This makes for easier debugging and could save sometimes when stumbling onto these kinds of troubles!
UPDATE realized that I hadn't completed with a nice class to encompass the tests.
Related
I'm trying to test the following view
def generate_exercise_edl(request, ex_pk, unit_pk, *args, **kwargs):
ex_instance = Exercises.objects.get(id=ex_pk)
unit_instance = Units.objects.get(id=unit_pk)
unit_edl = UnitEdl.objects.filter(unit=unit_instance)
locations = Locations.objects.all()
unit_edl = list(unit_edl)
print(request)
print(request.POST)
print(request.user)
if request.method == "POST":
for item in unit_edl:
ExerciseEdl.objects.update_or_create(unit=unit_instance, exercise=ex_instance, equipment=item.equipment,
quantity=item.quantity, location=Locations.objects.get(location="Okinawa"))
print(request)
return redirect('exercise-equipment', ex_pk=ex_pk, unit_pk=unit_pk)
else:
messages.error(
request, f'Failed to add/update the {unit_instance.unit_name} edl for {ex_instance.exercise}.')
context = {
'ex_instance': ex_instance,
'unit_instance': unit_instance,
'unit_edl': unit_edl,
'locations': locations,
}
return render(request, 'exercise/exercise_edl.html', context)
This is my test code
def test_generate_edl(self):
unit_edl = UnitEdl.objects.filter(unit=unit.pk)
for edl in unit_edl:
ExerciseEdl.objects.update_or_create(
unit=unit,
exercise=ex,
equipment=edl.equipment,
quantity=edl.quantity,
location=loc
)
response = self.client.post(
f'/exercise/{ex.pk}/edl/{unit.pk}/convert/')
ex_edl = ExerciseEdl.objects.all().count()
self.assertEquals(ex_edl, 2)
self.assertEqual(response.status_code, 302)
This is the URL for the view
path('exercise/<int:ex_pk>/edl/<int:unit_pk>/convert', views.generate_exercise_edl, name='generate-edl'),
And the part of the template that calls my function
<form action="{% url 'generate-edl' ex_pk=ex_instance.id unit_pk=unit_instance.id %}" method="post">
{% csrf_token %}
<input class="btn btn-primary btn-sm mt-2" type="submit" value="Generate EDL">
</form>
My test returns 404, not 302, but the function on the site works, and redirects you.
f'/exercise/{ex.pk}/edl/{unit.pk}/convert/' isn't mapped to any template, it's just the url for the function. In the past my tests have returned a status code of 404 when I wrote the post data incorrectly.
print(request.POST) returns:
<QueryDict: {'csrfmiddlewaretoken':
['ZYT0dgMZqqgmCo2OufdI9B0hIJ5k5qPKcxnkReWPZy0iY9McaBO7MHENjYLzH66O']}>
Which makes sense because I'm not sending any post data, just the csrf token.
What I want to know is, am I on the right track with using 'response = self.client.post(
f'/exercise/{ex.pk}/edl/{unit.pk}/convert/')'?
With my other tests I include the post data in a dictionary along with the URL, but this function doesn't use any, so I just ran a similar function.
Is there a better way to test this? Should I just refactor?
You need to use reverse to build your URL rather than hard coding it. Since you hard coded it, it is getting a 404 since the URL the test tried to post to is incorrect.
I don't know the app_name in your URLs file, you will need to add that to the reverse. For example if it was excercise it would be exercise:generate-edl.
from django.urls import reverse
response = self.client.post(reverse(
'<app_name>:generate-edl',
kwargs={
ex_pk: ex.pk,
unit_pk: unit.pk,
}
))
I'm using Django 3.2b1 and pytest 6.2.2.
I'm trying to use pytest to write a test to make sure admins are able to delete objects using the delete_selected action. My test looks like this:
def test_delete_mymodel_action(admin_client):
objs_to_delete = [
MyModel.objects.create(),
MyModel.objects.create(),
]
MyModel.objects.create() # Don't delete this obj
data = {
"action": "delete_selected",
"_selected_action": [str(f.pk) for f in objs_to_delete],
"post": "yes", # Skip the confirmation page
}
change_url = reverse("admin:myapp_mymodel_changelist")
admin_client.post(change_url, data)
assert MyModel.objects.count() == 1
The code works and ends in a 302 redirect back to the changelist, but the objects don't get deleted. The response is:
test_delete_mymodel_action - assert 3 == 1
The reason I'm testing this is that certain code can cause the delete_selected action to fail. For example, if you override get_queryset() in the ModelAdmin and return a queryset that uses distinct(), the delete_selected action will fail.
Here the code from a delete confirmation page in Django Admin:
<form method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="VCR7vjVYcb2xuMdPUknotrealViwj92wgZrT21k6RbqGxXNlQnCORU1Fp6NzKhn64">
<div>
<input type="hidden" name="_selected_action" value="31418">
<input type="hidden" name="_selected_action" value="31412">
<input type="hidden" name="action" value="delete_selected">
<input type="hidden" name="post" value="yes">
<input type="submit" value="Yes, I’m sure">
No, take me back
</div>
</form>
Some helpful references:
Django's delete_selected() method.
Testing custom admin actions in django SO Answer
I just run into the same problem. I noticed I was logged in with the wrong user.
My thought process:
302 doesn't indicate there's anything wrong, there was no content in the response either (b'')
I added follow=True to self.client.post. The response was 200 and there were no objects, so I assumed it worked correctly but it failed on assertion
I put breakpoint in delete_selected of django.contrib.admin.actions and n = queryset.count() was 0.
If it's not listed after but there's nothing to delete (n = 0), let's see if there was something to be deleted before.
response = self.client.get(reverse("admin:myapp_mymodel_changelist"))
self.assertContains(response, obj.id)
nope!
So the problem with your test is that these objects can't be deleted because they can't be retrieved, probably due to some filtering.
Note, Django admin won't raise 404 if the object has not been found.
I have a simple form:
{% block content %}
<p> Upload invoices </p>
<form method="post" action="{% url 'upload_thing ' %}" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" name="invoice_thing">
<button type="submit">Upload</button>
</form>
I have a view
#require_POST
#transaction.atomic
def upload_thing(request):
....
How do I make sure that the no one can hit the post endpoint via curl or postman?
I want the end point to be accessible only by hitting the form button. The only people who can do this are admin users.
How do I accomplish this?
To be honest, you shouldn't prevent curl or wget requests from django application. It can be done from a reverse proxy server, for example in NGINX you can put the following configuration:
if ($http_user_agent ~* (wget|curl) ) {
return 403;
}
Still, its not a proper protection, User-Agent information can be spoofed. More information can be found in this serverfault answer.
If you want to prevent people accessing this page who aren't admin, then you can simply put a restriction on the view like this:
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
#login_required
#require_POST
#transaction.atomic
def upload_thing(request):
if not request.user.is_superuser:
raise PermissionDenied()
# rest of the code
The right way to do this in python is via decorators.
from django.contrib.auth.decorators import login_required, user_passes_test
#user_passes_test(lambda user: user.is_superuser)
def upload_thing(request):
In this way you restrict the method and not the html page.
So only logged admin can use upload_thing (from curl, postman, or html form).
I have seen a number of forums and posts but still couldn't get the handle of it. Here in django doc, it says
The CSRF middleware is activated by default in the MIDDLEWARE setting. If you override that setting, remember that 'django.middleware.csrf.CsrfViewMiddleware' should come before any view > middleware that assume that CSRF attacks have been dealt with.
If you disabled it, which is not recommended, you can use csrf_protect() on particular views you want to protect (see below).
In any template that uses a POST form, use the csrf_token tag inside the > element if the form is for an internal URL, e.g.:
form action
{% csrf_token %}
Based on that, in my html template I did simply:
<form id='frm' name='frm' method="post" action="{% url 'gettip' %}" >
{% csrf_token %}
<input type="text" name="tipid" name="tipid">
<input type="submit" value="Get Tip Value"/>
</form>
I expected the CSRF_token to create the hidden element since the middleware is already loaded. I see no element in the form and I get CSRF error.
The form is not associated with any model. I haven't used forms.py either. My current view is simply to output something:
def gettip(request):
if request.POST:
return HttpResponse('You requested a tip')
#from a weblink, i was told to add the following but it made no difference
context = {}
return render_to_response('tip.html',context, context_instance=RequestContext(request))
The error I am getting is obviously CSRF missing cos the hidden element is not there at all.
I am migrating from PHP and this is giving me a hard time. Though my form is not for login purposes, I couldn't get this one to work either for the same error. I am on django 1.10 and just want to get a positive response when form is submitted.
Don't use render_to_response, it's obsolete. Use render instead.
from django.shortcuts import render
def gettip(request):
if request.POST:
return HttpResponse('You requested a tip')
context = {}
return render(request, 'tip.html', context)
If the template containing the form is rendered by another view, you'll have to fix that view as well.
I have been using udacity.com to learn to program but I ran into some problem with my code(python). The application is post to allow to put down the day you were born and when you push submit it's post to say "Thanks That's a totally valid day!". It's not doing it. The application just restarts again. Can some tell me why the is statement is not working and how to fix it.
import webapp2
form="""
<form>
What is your birthday?
<br>
<label>Month<input type="type" name="month"></label>
<label>Day<input type="type" name="day"></label>
<label>Year<input type="type" name="year"></label>
<div style="color: red">%(error)s</div>
<br>
<br>
<input type="submit">
</form>
"""
class MainPage(webapp2.RequestHandler):
def write_form(self, error=""):
self.response.out.write(form % {"error": error} )
def get(self):
self.write_form()
def post(self):
user_month = valid_month(self.request.get('month'))
user_day = valid_day(self.request.get('day'))
user_year = valid_year(self.request.get('year'))
if not (user_month and user_month and user_year):
self.write_form("That doesn't look valid to me, friend.")
else:
self.response.out.write("Thanks! That's a totally valid day!")
app = webapp2.WSGIApplication([('/',MainPage)], debug=True)
That fragment has a syntax error. If you're watching the terminal/console, you'll notice Python complain about the else (which is missing a trailing :). Watching the console, and understanding what it's saying, is key if you want to make progress.
With that fixed, you'll see the form. Then, when you trying posting, you'll get a "405 Method not allowed" because the post method is over-indented.
Add method="post" to your form tag just above the text "What is your birthday?". You are using post method to post data to the form but the button submit doesn't know that you are using post as the tag does not have method="post" in it.
Also check my answer on one of your other related post here. python post and get commands