Testing Django view - django

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,
}
))

Related

Unittest Flask-App CSRF-token missing after Post-Request

When unittesting my flask-app and sending post-requests with a Testclient I find a form.error that the CSRF token is missing.
I have been to this and to this post, also read the documentation, however I still cant get my Problem solved.
How the CSRF-Protection is created:
class Routes:
app = Flask(__name__)
app.config['SECRET_KEY'] = WebserverConfig().secret_key
CSRFProtect().init_app(__app)
The view to be tested:
#__app.route("/settings", methods=["GET", "POST"])
def settings():
form = SettingsForm()
if request.method == "POST":
if form.validate_on_submit():
Routes.__reqhandler.put_settings(form=form)
return redirect(url_for("settings"))
if form.errors != {}:
Routes.__reqhandler.error_put_settings(form=form)
data = Routes.__prepdata.prep_settings()
return render_template("settings.html", form=form, data=data)
The template which contains the form:
<form method="POST">
{{ form.hidden_tag() }}
<div class="text-center">
<h3>Einstellungen</h3>
</div>
{{ form.intervall.label }}
{{ form.intervall(id="intervall", class="form-control", placeholder=data["intervall"])}}
</form>
The fixture creating the testclient:
#pytest.fixture
def client():
app = Routes().get_app()
app.config["WTF_CSRF_METHODS"] = []
with app.test_client() as client:
yield client
The test:
def test_settings_valid_intervall(client):
res = client.post("settings", data={"intervall": "00:01:00"}, follow_redirects=True)
assert b'value="00:01:00' in res.data
When printing the data (print(res.data) an alert (created with a flash if there are errors in a form) with the info that "The CSRF token is missing" is sent. What am I missing to tell form.validate_on_submit to not check if a valid CSRF-token was passed?
I solved it by not only emptying the list of the WTF_CSRF_METHODS but also disabling CSRF within the fixture that creates the client:
app.config["WTF_CSRF_ENABLED"] = False

What is the correct way of setting the Action attribute in a Django form

I have an html form in a Django system and I cannot find the correct way to implement the Action attribute in the form
html
<form action="stages" method="post">
urls.py
urlpatterns = [
path('<str:partner_pk>/stages/', views.Stages.as_view(), name="stages"),
]
views.py
class Stages(View):
"""CBV for setting stages."""
url = 'duo/stages.html'
def get(self, request, partner_pk):
app.context['partner_pk'] = partner_pk
return render(request, self.url, app.context)
def post(self, request, partner_pk):
"""Render the POST request."""
stages_selected = app.update_stages(request)
if app.context['use_stages'] and not stages_selected:
messages.error(request, 'You have not selected any stages.')
return render(request, f'{partner_pk}', app.context)
else:
return HttpResponseRedirect(reverse(f'duo/{partner_pk}/'))
The error I get on clicking submit is:
RuntimeError at /duo/2/stages/stages
You called this URL via POST, but the URL doesn't end in a slash and you have APPEND_SLASH set...
I have tried various other urls in action but the all give an error message
[EDIT 200200828]
The APPEND_SLASH message is misleading. It is just the message that I get with the html
<form action="stages" method="post">
If I change the html to
<form action={% url 'stages' partner_pk %} method="post">
or
<form action='?' method="post">
I get the message
NoReverseMatch at /duo/2/stages/'
You haven't set APPEND_SLASH option so all of your routes should have / afterwards but still it wouldn't work as you are appending relative path (notice stages doubling in URL)
Considering you are posting on same route you could omit action attribute
<form method="post">
or use something like (as HTML5 standard does not allow empty action)
<form action='?' method="post">
additionally you could use full path something like this
<form action="{% url 'stages' partner_pk %}" method="post">
The problem in this case was the reverse(url)
I change the post method in views.py to
def post(self, request, partner_pk):
"""Render the POST request."""
stages_selected = app.update_stages(request)
if app.context['use_stages'] and not stages_selected:
messages.error(request, 'You have not selected any stages.')
return render(request, f'{partner_pk}', app.context)
else:
return HttpResponseRedirect(f'/duo/{partner_pk}')
and it works

receiving dropzone files in Django views

Could anyone help me to receive dropzone input in a function view in Django?
I wrote this code in my template for dropzone:
index.html:
<form method="post" action="{% url 'index' %}" enctype="multipart/form-data" class="dropzone" id="myDropzone" >
{% csrf_token %}
</form>
and for URLs.py:
url('^$', views.UI_index, name='index'),
and in views.py I receive the posted file by UI_index function:
def UI_index(request):
file_upload_message=''
if request.method=='post':
f = request.FILES['file']
file_upload_message='the file is received!'
return render(
request,
'cta_UI/index.html',
{'file_upload_message':file_upload_message},
)
in the index.html I receive the value of file_upload_message like {{file_upload_message}} but it doesn't seem to work properly, it seems that the condition of if request.method=='post' doesn't work, then how I can post the uploded file to a view? when the post metheod is exactly executed in dropzone?
Although it is not my real project it is the first step to receive a file in view...please help me by that.
Thanks.
request.method
you can try following code
def UI_index(request):
if request.method=='POST':
up_file = request.FILES['file']
if up_file:
destination = open('/tmp/' + up_file.name, 'wb+')
for chunk in up_file.chunks():
destination.write(chunk)
destination.close()
file_upload_message='the file is received!'
else:
file_upload_message='the file is missing!'
return render(request,'cta_UI/index.html',
{'file_upload_message':file_upload_message},)

django redirect to another view with context

I have this in my view
def foo(request):
context['bar'] = 'FooBar'
return redirect('app:view')
is there some way to include this context['bar'] when I redirect to 'app:view'? My last resort and alternative would be to do use render() however, I do not want to define all the context variables again. Is there any other approach to this?
I would use session variables in order to pass some context through a redirect. It is about the only way to do it outside of passing them as part of the url and it is the recommended django option.
def foo(request):
request.session['bar'] = 'FooBar'
return redirect('app:view')
#jinja
{{ request.session.bar }}
A potential pitfall was pointed out, whereas the session variable gets used incorrectly in a future request since it persists during the whole session. If this is the case you can fairly easily circumvent this problem in a future view in the situation it might be used again by adding.
if 'bar' in request.session:
del request.session['bar']
In django You can not pass parameters with redirect. Your only bet is to pass them as a part of URL.
def foo(request):
context['bar'] = 'FooBar'
redirect(reverse('app:view', kwargs={ 'bar': FooBar }))
in your html you can get them from URL.
you need to use HttpResponseRedirect instead
from django.http import HttpResponseRedirect
return HttpResponseRedirect(reverse('app:view', kwargs={'bar':FooBar}))
I was with the same problem. I would like to redirect to another page and show some message, in my case, it's a error message. To solve it, I used the django messages: https://docs.djangoproject.com/en/4.0/ref/contrib/messages/
I did this:
def foo(request):
message.error(request, 'bla bla bla')
return redirect('foo_page')
In my template foo_page.html:
{% if messages %}
{% for message in messages %}
<div class={{ message.tags }}>{{ message }}</div>
{% endfor %}
{% endif %}
What most work for me is:
next_url = '/'
url = reverse('app:view')
url = f'{url}?next={next_url}&'
return redirect (url)
I just had the same issue, and here is my solution.
I use Django messages to store my parameter.
In template I do not read it with template tag {% for message in messages %}, but rather do POST request to my API to check if there are any messages for me.
views.py
def foo(request):
messages.success(request, 'parameter')
return redirect('app:view')
api/views.py
#api_view(['POST'])
#login_required
def messageList(request):
data = {}
messageList = []
storage = messages.get_messages(request)
for message in storage:
msgObj = makeMessage(message.message, tag=message.level_tag)
messageList.append(msgObj['message'])
data['messages'] = messageList
return Response(data)

Django testing a DeleteView

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.