I've been unable resolve this issue on IRC, hoping I could find some guidance here. I have the following test:
def test_validation_errors_return_hops_list_page(self):
response = self.client.post(
'/beerdb/add/hops',
data={
'name': '',
'min_alpha_acid': '',
'max_alpha_acid': '',
'country': '',
'comments': ''
}, follow=True
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'homebrewdatabase/addhops.html')
name_validation_error = escape("A hop name is required")
min_alpha_acid_error = escape("You must enter a min alpha acid")
max_alpha_acid_error = escape("You must enter a max alpha acid")
country_error = escape("You must enter a country")
comments_error = escape("You must enter a comment")
self.assertContains(response, name_validation_error)
self.assertContains(response, min_alpha_acid_error)
self.assertContains(response, max_alpha_acid_error)
self.assertContains(response,country_error)
self.assertContains(response, comments_error)
It's failing on self.assertContains(response, name_validation_error). Here's the trace back:
Failure
Traceback (most recent call last):
File "/Users/USER/workspace/PycharmProjects/hashtagbrews/homebrewdatabase/tests/test_views.py", line 189, in test_validation_errors_return_hops_list_page
self.assertContains(response, name_validation_error)
File "/Users/USER/workspace/Envs/hashtagbrews/lib/python3.4/site-packages/django/test/testcases.py", line 398, in assertContains
msg_prefix + "Couldn't find %s in response" % text_repr)
AssertionError: False is not true : Couldn't find 'A hop name is required' in response
My view in views.py renders the hops.html template with errors when the form is invalid:
def addhops(request):
add_form = HopForm(request.POST or None)
if request.method == 'POST':
if add_form.is_valid():
Hop.objects.create(name=request.POST['name'],
min_alpha_acid=request.POST['min_alpha_acid'],
max_alpha_acid=request.POST['max_alpha_acid'],
country=request.POST['country'],
comments=request.POST['comments']
)
return redirect('hops_list')
else:
hops_list = Hop.objects.all()
return render(request, 'homebrewdatabase/hops.html', {'hops': hops_list, 'form': add_form})
return render(request, 'homebrewdatabase/addhops.html', {'form': add_form})
When I manually click through the site, I get exactly what I'm looking for: a redirect from the modal to the main hops page list with a Bootstrap alert box at the top containing an unordered list of errors from add_hops.errors. I've printed out the response after the post request (self.client.post('url', data={invalid data})) and it only contains the modal form. What would be the proper method to finish this test? Or do I need to rewrite my form validation?
The issue here, as identified in the comments, is that the Django test client runs a GET request on the addhops view method after executing the post request. According to the view logic, if the method isn't POST, then it returns the bootstrap modal, which does not contain the form errors by design. So the test will fail when using the test client. However, the test can be altered to, instead, use the HttpRequest object to send invalid data in a POST request and then assert that the content contains the form errors. So I rewrote the test to use the following, which passes--
def test_validation_errors_return_hops_list_page(self):
request = HttpRequest()
request.method = 'POST'
request.POST['name'] = ''
request.POST['min_alpha_acid'] = ''
request.POST['max_alpha_acid'] = ''
request.POST['country'] = 'USA'
request.POST['comments'] = ''
response = addhops(request)
self.assertEqual(response.status_code, 200)
name_validation_error = escape("A hop name is required")
min_alpha_acid_error = escape("You must enter a min alpha acid")
max_alpha_acid_error = escape("You must enter a max alpha acid")
comments_error = escape("You must enter a comment")
self.assertContains(response, name_validation_error)
self.assertContains(response, min_alpha_acid_error)
self.assertContains(response, max_alpha_acid_error)
self.assertContains(response, comments_error)
I cannot assert which template was used as assertTemplateUsed is a method that belongs to the test client, but my functional tests with selenium should be enough to check that the required elements are in the rendered view. This may have to be changed in the future, but for now, it's enough to work with.
Related
I'm learning full stack in Flask and am having trouble with a particular route from an API. The API being developed is a list of books and in particular I am trying to reach the data for a particular book, say book with ID = 8. The URI is http://127.0.0.1:5000/books/8. However this returns a 400 error (bad request).
I really can't spot what is going wrong. I have defined the route '/books/int:book_id' with methods GET and PATCH, so I would expect the route to work. I also see errors when I test the route with curl, for example:
curl -X PATCH -H "Content-Type: application/json" -d '{"rating":"1"}' http://127.0.0.1:5000/books/8
See below for the particular route in question:
#app.route('/books/<int:book_id>', methods=['GET', 'PATCH'])
def update_book_rating(book_id):
body = request.get_json()
try:
book = Book.query.filter_by(Book.id==book_id).one_or_none()
if book is None:
abort(404)
if 'rating' in body:
book.rating = int(body.get('rating'))
book.update() #Class book in models.py has an update method which executes a commit()
return jsonify({
'success': True,
'id': book.id
})
except Exception as e:
print(e)
abort(400)
If it helps, I am also adding the full code. Note that the Book object is defined in a separate file, which I won't put here.
import os
from flask import Flask, request, abort, jsonify
from flask_sqlalchemy import SQLAlchemy # , or_
from flask_cors import CORS
import random
from models import setup_db, Book
BOOKS_PER_SHELF = 8
# #TODO: General Instructions
# - As you're creating endpoints, define them and then search for 'TODO' within the frontend to update the endpoints there.
# If you do not update the endpoints, the lab will not work - of no fault of your API code!
# - Make sure for each route that you're thinking through when to abort and with which kind of error
# - If you change any of the response body keys, make sure you update the frontend to correspond.
def paginate_books(request, selection):
page = request.args.get('page', 1, type=int)
start = (page - 1) * BOOKS_PER_SHELF
end = start + BOOKS_PER_SHELF
books = [book.format() for book in selection]
current_books = books[start:end]
return current_books
def create_app(test_config=None):
# create and configure the app
app = Flask(__name__)
setup_db(app)
CORS(app)
# CORS Headers
#app.after_request
def after_request(response):
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization,true")
response.headers.add("Access-Control-Allow-Methods", "GET,PUT,PATCH,POST,DELETE,OPTIONS")
return response
# #TODO: Write a route that retrivies all books, paginated.
# You can use the constant above to paginate by eight books.
# If you decide to change the number of books per page,
# update the frontend to handle additional books in the styling and pagination
# Response body keys: 'success', 'books' and 'total_books'
# TEST: When completed, the webpage will display books including title, author, and rating shown as stars
#app.route('/books', methods=['GET'])
def get_books():
selection = Book.query.order_by(Book.id).all()
current_books = paginate_books(request, selection)
if len(current_books) == 0:
abort(404)
return jsonify({
'success': True,
'books': current_books,
'total_books': len(Book.query.all())
})
# #TODO: Write a route that will update a single book's rating.
# It should only be able to update the rating, not the entire representation
# and should follow API design principles regarding method and route.
# Response body keys: 'success'
# TEST: When completed, you will be able to click on stars to update a book's rating and it will persist after refresh
#app.route('/books/<int:book_id>', methods=['GET', 'PATCH'])
def update_book_rating(book_id):
body = request.get_json()
try:
book = Book.query.filter_by(Book.id==book_id).one_or_none()
if book is None:
abort(404)
if 'rating' in body:
book.rating = int(body.get('rating'))
book.update() #Class book in models.py has an update method which executes a commit()
return jsonify({
'success': True,
'id': book.id
})
except Exception as e:
print(e)
abort(400)
# #TODO: Write a route that will delete a single book.
# Response body keys: 'success', 'deleted'(id of deleted book), 'books' and 'total_books'
# Response body keys: 'success', 'books' and 'total_books'
#app.route('/delete/<int:book_id>', methods=['DELETE'])
def delete_book(book_id):
try:
book = Book.query.filter_by(Book.id==book_id).one_or_none()
if book is None:
abort(404)
book.delete()
selection = Book.query.order_by(Book.id).all()
current_books = paginate_books(request, selection)
return jsonify({
'success': True,
'deleted': book_id,
'books': current_books,
'total_books': len(Book.query.all())
})
except:
abort(422)
# TEST: When completed, you will be able to delete a single book by clicking on the trashcan.
# #TODO: Write a route that create a new book.
# Response body keys: 'success', 'created'(id of created book), 'books' and 'total_books'
# TEST: When completed, you will be able to a new book using the form. Try doing so from the last page of books.
# Your new book should show up immediately after you submit it at the end of the page.
#app.route('/books', methods=['POST'])
def create_book():
body = request.get_json()
new_title = body.get('title', None)
new_author = body.get('author', None)
new_rating = body.get('rating', None)
try:
book = Book(title=new_title, author=new_author, rating=new_rating)
book.insert()
selection = Book.query.order_by(Book.id).all()
current_books = paginate_books(request, selection)
return jsonify({
'success': True,
'created': book.id,
'books': current_books,
'total_books': len(Book.query.all())
})
except:
abort(422)
#app.errorhandler(400)
def bad_request(error):
return jsonify({
'success': False,
'error': 400,
'message': 'Server cannot or will not process the request due to client error (for example, malformed request syntax, invalid request message framing, or deceptive request routing).'
}), 400
#app.errorhandler(404)
def not_found(error):
return jsonify({
'success': False,
'error': 404,
'message': 'resource not found'
}), 404
#app.errorhandler(405)
def not_found(error):
return jsonify({
'success': False,
'error': 405,
'message': 'method not allowed'
}), 405
#app.errorhandler(422)
def unprocessable(error):
return jsonify({
'success': False,
'error': 422,
'message': 'unprocessable'
}), 422
return app
I'm trying to test my post with a testing suite. I've just been trying to follow the documentation to do this. The main problem I'm having right now is that response.context is returning None.
This is what my test class looks like:
class JSONHandlerTester(TestCase):
def setUp(self):
self.client = Client()
self.jsonTestPath = os.path.join(settings.MEDIA_ROOT,'json','jsonTests')
def testing(self):
for test in os.listdir(self.jsonTestPath):
testFile = os.path.join(os.path.join(self.jsonTestPath),test)
split = test.split('.')
testName = split[0]
testNameArray = re.findall('[a-zA-z][^A-Z]*', testName)
project = testNameArray[0]
team = testNameArray[1]
with open(testFile) as json:
response = self.client.post('/JSONChecker', {'json_project': project, 'json_team': team, 'json': json})
print response
print response.context
if (response.context['title'] == "Congratulations!!! Your JSON Passes!!!" and testNameArray[2] == "Pass") or (response.context['title'][2:] == "The" and testNameArray[2] == "Fail"):
print test+': Works'
else:
print test+': BREAKS: PROBLEM DETECTED'
Also this is what my render looks like:
return render(request, 'JSONChecker.html',context = {'title': title, 'validationErrors':validationErrors,'errors':errors, 'isLoggedIn':isLoggedIn, 'form': form, 'post':post})
If the form is invalid or the extension isn't json this is what the render looks like (this shouldn't be triggered by suite):
return render(
request,
'JSONChecker.html',
context = {'title': title,'errors': errors,'isLoggedIn':isLoggedIn,'team':team, 'email':email,'form':form, 'post': post},
)
Content-Length: 0
Content-Type: text/html; charset=utf-8
Location: /JSONChecker/
I'm using Django 1.11 and Python 2.7
Context attribute is only populated when using the DjangoTemplates backend.
I am testing my views using Django Unit testing. I am making get and post requests with params to check what status i get back.
But the problem how to check for context variables which are retuned in the response?
For example, on the View Cities page, I make a get request, the context dict in the view has the variable cities. So I want to check for context.
resp = self.client.post(
path=reverse('upload_video'),
data={"video_url": video_url, "pro": 1}
)
self.assertEqual(resp.status_code, 200)
Condition is True both ways, if the form is invalid or valid it returns 200. If I can check for context, then I can check what has been retuned from the view in response.
What I tried
=> resp.__dict__
{'templates': [], '_handler_class': None, '_headers': {'vary': ('Vary', 'Cookie'), 'content-type': ('Content-Type', 'application/json')}, '_charset': 'utf-8', '_closable_objects': [], 'cookies': <SimpleCookie: >, 'client': <django.test.client.Client object at 0x112bace10>, '_base_content_is_iter': False, 'context': None, 'request': {u'CONTENT_LENGTH': 202, u'wsgi.input': <django.test.client.FakePayload object at 0x113667990>, u'REQUEST_METHOD': 'POST', u'PATH_INFO': '/upload/video/modal/', u'CONTENT_TYPE': u'multipart/form-data; boundary=BoUnDaRyStRiNg', u'QUERY_STRING': ''}, '_container': ['{"error": {"msg": "Pro: Select a valid choice. That choice is not one of the available choices.", "head": null}}']}
Check _container has that variable. The form is invalidated, and retuned an error in the context. but when I do the following i get None
=> resp.context
None
Test
import os
from django.contrib.auth import authenticate
from django.core.urlresolvers import reverse
from django.test import TestCase
def test_video_upload(self):
""" Test that video upload is successful """
self.create_and_login(username="su", password="su", is_superuser=True)
video_urls = [
u"https://www.youtube.com/watch?v=abc",
u"https://vimeo.com/32222",
u"http://www.dailymotion.com/video/rer"
]
for video_url in video_urls:
resp = self.client.post(
path=reverse('upload_video'),
data={"video_url": video_url, "pro": 1}
)
set_trace() #Breakpoint
a = resp.context[-1] # <=== Not getting it here.
self.assertEqual(resp.status_code, 200) #passes
videos = Video.objects.all()
self.assertEqual(len(videos), 3)
View
ctx = {}
if request.method == Enums.Request.POST:
video_form = UploadVideoEasyForm(data=request.POST)
if video_form.is_valid():
video, log = video_form.save(request=request)
msg = 'Successfully Uploaded, View: here'.format(video.get_absolute_url())
ctx[Enums.ResponseAlert.Success] = {'msg': msg}
else:
ctx[Enums.ResponseAlert.Error] = make_alert(msg=form_error_to_string(video_form))
return HttpResponse(json.dumps(ctx), content_type="application/json")
elif request.method == Enums.Request.GET:
ctx['upload_video'] = UploadVideoEasyForm()
if request.user.is_authenticated() and request.user.is_superuser:
return render_to_response('new/modals/upload_video.html', context_instance=RequestContext(request, ctx))
Cheers.
The resp (An instance of django.test.Response) should have an context attribute.
You can access context value using context[..]:
self.assertEqual(resp.context['cities'], ...)
I have a method in Django where I get POST data from a mobile app and all I do is save it and send a Response. The problem is though the the data gets saved but no matter what the app receives the response code 500.
<Response [500]>
code:
#csrf_exempt
def store_recordings(request):
if request.method == 'POST':
print "In POST",request.POST
driverName = request.POST['driverName']
driverMobileNum = request.POST['driverMobileNum']
customerMobileNum = request.POST['customerMobileNum']
salesOrderNo = request.POST['salesOrderNo']
callRecord = request.POST['callRecord']
latitude = request.POST['latitude']
longitude = request.POST['longitude']
callStart = request.POST['callStart']
callEnd = request.POST['callEnd']
callDuration = request.POST['callDuration']
callType = request.POST['callType']
driverrecording = DriverRecording(driverName=driverName,driverMobileNum=driverMobileNum,customerMobileNum=customerMobileNum,salesOrderNo=salesOrderNo,callRecord=callRecord,latitude=latitude,longitude=longitude,callStart=callStart,callEnd=callEnd,callDuration=callDuration,callType=callType)
save_or_not = driverrecording.save()
driverexist = DriverNames.objects.all()
new_driver_flag = False
driverName_list = [each.driverName for each in driverexist]
driverName_list = list(set(driverName_list))
if driverName in driverName_list:
pass
else:
DriverNames(driverName=driverName,driverMobileNum=driverMobileNum).save()
return HttpResponse(status=201)
else:
return HttpResponse(status=400)
I am perplexed what is the problem.
Thanks.
Almost certainly, one or more of those fields is not being sent, so you are getting a KeyError. If you set DEBUG to True you would see the traceback.
You should be using Django's forms framework, instead of directly accessing the POST data. That will validate the input and allow you to display any errors.
I am trying to get a login form I have in django to only allow three login attempts before redirecting to a "login help" page. I am currently using the builtin "django.contrib.auth.views.login" view with a custom template. How do I force it to redirect to another page after n failed login attempts?
There's actually a project out there which provides a Django middleware to do just this, called django-axes. Simply install it with the instructions provided and then set AXES_LOGIN_FAILURE_LIMIT to the number of login attempts you want before a record is created for the failed logins. You'll still have to check this record when you want to lock someone out, however.
i use django-brake and memcached
#ratelimit(field='email', method='POST', rate='5/m')
#ratelimit(field='email', method='POST', rate='10/h')
#ratelimit(field='email', method='POST', rate='20/d')
def login_failure(request, login_form):
"""
Increment cache counters, 403 if over limit.
"""
was_limited = getattr(request, 'limited', False)
if was_limited:
limits = getattr(request, 'limits', [])
login_form.full_clean()
login_form._errors.setdefault(NON_FIELD_ERRORS, ErrorList())\
.append('accout locked, try '+ str(limits[0]['period']) + ' seconds later')
return render(request, 'user/login.html', {'form': login_form})
def login(request):
if request.method == 'GET':
next = request.GET.get('next', '')
return render(request, 'user/login.html', {'next': next})
elif request.method == 'POST':
login_form = LoginForm(request.POST)
# check first
from brake.utils import get_limits
limits = get_limits(request, 'login_failure', 'email', [60, 3600, 86400])
if limits:
login_form.full_clean()
login_form._errors.setdefault(NON_FIELD_ERRORS, ErrorList())\
.append('accout locked, try '+ str(limits[0]['period']) + ' seconds later')
return render(request, 'user/login.html', {'form': login_form})
if login_form.is_valid():
email = login_form.cleaned_data['email']
submit_pwd = login_form.cleaned_data['password']
user = authenticate(username=email, password=submit_pwd)
if user is None:
#
res = login_failure(request, login_form)
if res is None:
login_form._errors.setdefault(NON_FIELD_ERRORS, ErrorList()).append('password wrong')
res = render(request, 'user/login.html', {'form': login_form})
return res
...
login etc...
else:
...
You could save a session if the user has failed to login.
request.SESSION['login_tries'] = 1
and if they fail to login again
request.SESSioN['login_tries'] = 2
If the session becomes equal to the amount of login tries you want them tho have, then do something.