Date post: | 14-Apr-2018 |
Category: |
Documents |
Upload: | walter-angolar-da-silva |
View: | 215 times |
Download: | 0 times |
of 81
7/30/2019 Advanced Django2929
1/81
Advanced Django
Simon Willisonhttp://simonwillison.net/
PyCon UK, 8th September 2007
http://simonwillison.net/http://simonwillison.net/7/30/2019 Advanced Django2929
2/81
Unit testingnewforms
Ajax
And if we have time... OpenID
Todays topics
7/30/2019 Advanced Django2929
3/81
Unit testing
7/30/2019 Advanced Django2929
4/81
Hard core TDD
Test Driven Development as taught by Kent
BeckWrite new code only if an automated test hasfailed
Revolutionises the way you write codePretty hard to adopt
7/30/2019 Advanced Django2929
5/81
I dont do test drivendevelopment. I do stupidity
driven testing... I wait until I dosomething stupid, and then write
tests to avoid doing it again.
Titus Brown
7/30/2019 Advanced Django2929
6/81
What NOT to do
No matter how tough the deadline, don't letyour test suite start breaking and say Ill fix itlater
This will hurt. A lot.
7/30/2019 Advanced Django2929
7/81
Testing in Django
Testing web apps is HARD, but Django helps outwith a bunch of features:
Fixtures
Doctests
Test ClientE-mail capture
7/30/2019 Advanced Django2929
8/81
Doctests
Used by Django for both ORM testing andgenerated documentation
You are encouraged to add them to your ownmodels.py
manage.py test to execute them
Great for regression tests - just copy and pastefrom an interpreter session
7/30/2019 Advanced Django2929
9/81
>>> p = Person(name="Bob", dob=date(1980, 1, 1))
>>> p.age(date(1980, 1, 1))0>>> p.age(date(1979, 1, 1))-1>>> p.age(date(1981, 6, 1))1>>> p.age(date(2000, 1, 1))20>>> p.age(date(1999, 12, 31))19>>> p2 = Person(name="Barry", dob=date(1981, 5, 5))>>> p2.age(date(1981, 5, 5))0>>> p2.age(date(1982, 5, 4))0
>>> p2.age(date(1982, 5, 5))1>>> p2.age(date(1982, 5, 6))1
7/30/2019 Advanced Django2929
10/81
class Person(models.Model):"""... tests here ...
"""name = models.CharField(maxlength=100)dob = models.DateField()def age(self, age=False):
return 1
7/30/2019 Advanced Django2929
11/81
$ python manage.py test
...F
======================================================================
FAIL: Doctest: peopleage.models.Person
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib/python2.5/site-packages/django/test/_doctest.py", line 2161, in runTest
raise self.failureException(self.format_failure(new.getvalue()))
AssertionError: Failed doctest test for peopleage.models.PersonFile "/home/simon/Projects/Django/oscon07/peopleage/models.py", line 4, in Person
----------------------------------------------------------------------
File "/home/simon/Projects/Django/oscon07/peopleage/models.py", line 7, inpeopleage.models.Person
Failed example:
p.age(date(1980, 1, 1))
Expected:0
Got:
1
7/30/2019 Advanced Django2929
12/81
def age(self, age=False):
if not age:age = date.today()delta = age - self.dobreturn int(math.floor(delta.days / float(365)))
7/30/2019 Advanced Django2929
13/81
File "/home/simon/Projects/Django/oscon07/peopleage/models.py", line 16,in peopleage.models.Person
Failed example:
p.age(date(1999, 12, 31))Expected:
19Got:
20
7/30/2019 Advanced Django2929
14/81
def age(self, age=False):
if not age:
age = date.today()
years = age.year - self.dob.yearthis_year_birthday = self.dob.replace(year=age.year)
birthday_has_passed = age >= this_year_birthday
if not birthday_has_passed:
years = years - 1
return years
7/30/2019 Advanced Django2929
15/81
Fixtures
Its useful to be able to clear and populate yourtest database in between tests
Fixtures let you do that (they also let youpopulate your database with real data when you
deploy your application)
7/30/2019 Advanced Django2929
16/81
Denormalisation
7/30/2019 Advanced Django2929
17/81
Normalised data is
for sissies
Cal Henderson
7/30/2019 Advanced Django2929
18/81
A forum, where each thread can have oneor more replies
Maintain a separate counter in the Forumtable of number of replies, to speed upqueries
7/30/2019 Advanced Django2929
19/81
class Thread(models.Model):subject = models.CharField(maxlength=100)
num_replies = models.IntegerField(default=0)
class Reply(models.Model):thread = models.ForeignKey(Thread)message = models.TextField()
7/30/2019 Advanced Django2929
20/81
[{ "model": "forum.thread","pk": "1",
"fields": {"num_replies": 0,"subject": "First thread" }
},{ "model": "forum.thread","pk": "2",
"fields": {"num_replies": 1,"subject": "Second thread" }
},{ "model": "forum.reply","pk": "1","fields": {
"thread": 2,"message": "First post!1" }
}]
7/30/2019 Advanced Django2929
21/81
from django.test import TestCase
from models import Thread, Reply
class ThreadCountTestCase(TestCase):fixtures = ['threadcount.json']
def test_add_reply(self):thread = Thread.objects.get(pk=2)
self.assertEqual(thread.num_replies, 1)thread.reply_set.create(message="Another post")
thread = Thread.objects.get(pk=2)self.assertEqual(thread.reply_set.count(), 2)self.assertEqual(thread.num_replies, 2)
def test_delete_reply(self):thread = Thread.objects.get(pk=2)
self.assertEqual(thread.num_replies, 1)
Reply.objects.get(pk=1).delete()thread = Thread.objects.get(pk=2)
self.assertEqual(thread.reply_set.count(), 0)self.assertEqual(thread.num_replies, 0)
7/30/2019 Advanced Django2929
22/81
======================================================
FAIL: test_add_reply (forum.tests.ThreadCountTestCase)----------------------------------------------------------------------Traceback (most recent call last):
File "/home/simon/Projects/Django/oscon07/forum/tests.py", line 16, intest_add_reply
self.assertEqual(thread.num_replies, 2)AssertionError: 1 != 2
======================================================
FAIL: test_delete_reply (forum.tests.ThreadCountTestCase)----------------------------------------------------------------------Traceback (most recent call last):
File "/home/simon/Projects/Django/oscon07/forum/tests.py", line 23, intest_delete_reply
self.assertEqual(thread.num_replies, 0)AssertionError: 1 != 0
----------------------------------------------------------------------
7/30/2019 Advanced Django2929
23/81
class Reply(models.Model):...def save(self):
super(Reply, self).save()
self.thread.num_replies = self.thread.reply_set.count()self.thread.save()
def delete(self):super(Reply, self).delete()self.thread.num_replies = self.thread.reply_set.count()self.thread.save()
7/30/2019 Advanced Django2929
24/81
.......
-----------------------------------------------------------Ran 7 tests in 0.372s
OK
7/30/2019 Advanced Django2929
25/81
Djangos TestClient lets you simulate a browserinteracting with your site
It also provides hooks in to the underlyingapplication, so you can test against the template
and context that was used to generate a page
Testing views
7/30/2019 Advanced Django2929
26/81
from django.test import TestCase
class RegistrationTest(TestCase):def test_slash(self):
response = self.client.get('/register')self.assertEqual(response.status_code, 301)
response = self.client.get('/register/')self.assertEqual(response.status_code, 200)
def test_register(self):
response = self.client.get('/register/')self.assertEqual(response.template[0].name, 'register.html')
self.assertTemplateUsed(response, 'register.html')
7/30/2019 Advanced Django2929
27/81
def test_signup_done_page(self):self.assertEqual(len(mail.outbox), 0)data = {
'email': '[email protected]', 'username': 'example','firstname': 'Example', 'lastname': 'User','password': 'password1', 'password2': 'password1',
}response = self.client.post('/signup/', data)self.assertEquals(response.status_code, 302)self.assertEquals(response['Location'], '/welcome/')# Check that the confirmation e-mail was sentself.assertEqual(len(mail.outbox), 1)sent = mail.outbox[0]
self.assertEqual(sent.subject, 'Welcome to example.com')
mailto:[email protected]:[email protected]:[email protected]7/30/2019 Advanced Django2929
28/81
http://www.djangoproject.com/documentation/testing/
More on testing with Django:
http://www.djangoproject.com/documentation/testing/http://www.djangoproject.com/documentation/testing/7/30/2019 Advanced Django2929
29/81
newforms
7/30/2019 Advanced Django2929
30/81
Display a form
User fills it in and submits it
Validate their entered data
If errors, redisplay form with previouslyentered data and contextual error messages
Continue until their submission is valid
Convert submission to appropriate Python types
The perfect form
7/30/2019 Advanced Django2929
31/81
Manipulators
7/30/2019 Advanced Django2929
32/81
Manipulatorsnewforms
7/30/2019 Advanced Django2929
33/81
from django import newforms as forms
class UserProfileForm(forms.Form):name = forms.CharField(max_length=100)email = forms.EmailField()bio = forms.CharField(widget=forms.Textarea)dob = forms.DateField(required=False)receive_newsletter = forms.BooleanField(required=False)
Forms are declarative
7/30/2019 Advanced Django2929
34/81
7/30/2019 Advanced Django2929
35/81
def create_profile(request):if request.method == 'POST':
form = UserProfileForm(request.POST)if form.is_valid():
# ... save the users profilereturn HttpResponseRedirect('/profile/saved/')
else:form = UserProfileForm()
return render_to_response('profile.html', {'form': form})
Simple form handling view
7/30/2019 Advanced Django2929
36/81
form = UserProfileForm(initial = {
'name': 'Simon Willison','email': '[email protected]',
})
Initial data
mailto:[email protected]:[email protected]:[email protected]7/30/2019 Advanced Django2929
37/81
ul.errorlist { color: red; }
...{{ form.as_p }}...
Simple template
7/30/2019 Advanced Django2929
38/81
Output
Name:
Email:
Bio:
Dob:
Receive newsletter:
7/30/2019 Advanced Django2929
39/81
Output
Name:
Email:
Bio:
Dob:
Receive newsletter:
7/30/2019 Advanced Django2929
40/81
Name:
Email:
Bio:
Dob:
Receive newsletter:
Output
7/30/2019 Advanced Django2929
41/81
Custom validationfrom django import newforms as forms
from django.newforms.util import ValidationError
class UserProfileForm(forms.Form):name = forms.CharField(max_length=100)
email = forms.EmailField()bio = forms.CharField(widget=forms.Textarea)
dob = forms.DateField(required=False)receive_newsletter = forms.BooleanField(required=False)
def clean_email(self):if self.cleaned_data['email'].split('@')[1] == 'hotmail.com':
raise ValidationError, "No hotmail.com emails, please."
7/30/2019 Advanced Django2929
42/81
Custom validationfrom django import newforms as forms
from django.newforms.util import ValidationError
class UserProfileForm(forms.Form):name = forms.CharField(max_length=100)
email = forms.EmailField()bio = forms.CharField(widget=forms.Textarea)
dob = forms.DateField(required=False)receive_newsletter = forms.BooleanField(required=False)
def clean_email(self):if self.cleaned_data['email'].split('@')[1] == 'hotmail.com':
raise ValidationError, "No hotmail.com emails, please."
7/30/2019 Advanced Django2929
43/81
Custom rendering
Email: {{ form.email }}{{ form.email.errors }}
Your e-mail address.
...
7/30/2019 Advanced Django2929
44/81
Model shortcuts
DRY: Youve already declared your models; youshouldnt have to repeat yourself in your forms
UserForm = form_for_model(User)
###############################
page = Page.objects.get(pk=1)PageForm = form_for_instance(page)
form = PageForm(request.POST)...if form.is_valid(): form.save()
7/30/2019 Advanced Django2929
45/81
http://www.djangoproject.com/documentation/newforms/
django/trunk/tests/regressiontests/forms/tests.py
Full documentation:
http://www.djangoproject.com/documentation/testing/http://www.djangoproject.com/documentation/testing/http://www.djangoproject.com/documentation/testing/http://www.djangoproject.com/documentation/testing/http://www.djangoproject.com/documentation/testing/http://www.djangoproject.com/documentation/testing/7/30/2019 Advanced Django2929
46/81
Ajax
7/30/2019 Advanced Django2929
47/81
First things first
If you're going to do Ajax, you need a JavaScriptlibrary
You could use Yet Another XMLHttpRequestabstraction... but the popular libraries offer fantasticconvenience
Good libraries include YUI, Dojo, MochiKit and(controversial) Prototype...
7/30/2019 Advanced Django2929
48/81
.. and jQuery
I'm going to be using jQuery
Almost everything is done in terms of CSSselectors and chained methods
It looks like a gimmick, but it isn't
http://simonwillison.net/2007/Aug/15/jquery/
http://simonwillison.net/2007/Aug/15/jquery/http://simonwillison.net/2007/Aug/15/jquery/http://simonwillison.net/2007/Aug/15/jquery/7/30/2019 Advanced Django2929
49/81
Ajax formats...
Django has great support for any and every Ajax
formatHTML fragments
XML
JSON
7/30/2019 Advanced Django2929
50/81
Username available?
from django.contrib.auth.models import User
def check_username(request):
reply = ""username = request.GET.get('username', '')
if username:
if User.objects.filter(username=username).count():
reply = 'Unavailable'
else:reply = 'Available'
return HttpResponse(reply)
7/30/2019 Advanced Django2929
51/81
jQuery('span#msg').load('/check_username/?username=' + input.val()
);
7/30/2019 Advanced Django2929
52/81
$('span#msg').load('/check_username/?username=' + input.val()
);
7/30/2019 Advanced Django2929
53/81
var input = $('input#id_username')input.keyup(function() {
$('span#msg').load('/check_username/?username=' + input.val()
);});
7/30/2019 Advanced Django2929
54/81
$(document).ready(function() {var input = $('input#id_username')input.keyup(function() {
$('span#msg').load('/check_username/?username=' + input.val()
);});
});
7/30/2019 Advanced Django2929
55/81
$(function() {var input = $('input#id_username')input.keyup(function() {
$('span#msg').load('/check_username/?username=' + input.val()
);});
});
7/30/2019 Advanced Django2929
56/81
Recycling server-sideform validation
7/30/2019 Advanced Django2929
57/81
from django import newforms as forms
class ContactForm(forms.Form):subject = forms.CharField(max_length=100)message = forms.CharField(widget=forms.Textarea())sender = forms.EmailField()
7/30/2019 Advanced Django2929
58/81
def validate_contact(request):"Validate post data, return errors as json"form = ContactForm(request.POST)if (request.GET.has_key('field')):
# Validate a single field
errors = form.errors[request.GET['field']]else:
errors = form.errorsreturn JsonResponse({
'valid': not errors,'errors': errors
})
7/30/2019 Advanced Django2929
59/81
from django.utils import simplejson
class JsonResponse(HttpResponse):
def __init__(self, data):HttpResponse.__init__(
self, simplejson.dumps(data),mimetype='application/json'
)
7/30/2019 Advanced Django2929
60/81
function validateInput(input) {
$.post('/contact/validate/?field=' + input.attr('id').replace('id_', ''),
$('form').formToArray(), function(data) {
var json = eval('(' + data + ')');
showErrors(input, json.errors);
}
);}
$(function() {
$(':input').blur(function() {
validateInput($(this));
});
});
7/30/2019 Advanced Django2929
61/81
function relatedErrorList(input) {
var prevUL = $(input).parent().prev();
if (prevUL && prevUL.attr('class') == 'errorlist') {return prevUL;
}
var errorlist = $('');
input.parent().before(errorlist);
return errorlist;
}
function showErrors(input, errors) {
var errorlist = relatedErrorList(input);
errorlist.empty();
$.each(errors, function(i, error) {
errorlist.append('' + error + '');});
}
7/30/2019 Advanced Django2929
62/81
Django often gets marked down in framework
comparisons due to the lack of built in Ajaxsupport
Personally I think that shipping without a
recommended library is a feature, not a bug
Django philosophy
7/30/2019 Advanced Django2929
63/81
(bonus section)
7/30/2019 Advanced Django2929
64/81
What is OpenID?
7/30/2019 Advanced Django2929
65/81
OpenID is adecentralised
mechanism for Single Sign On
7/30/2019 Advanced Django2929
66/81
An OpenID is a URL
http://simonwillison.myopenid.com/
http://simonwillison.net/
http://swillison.livejournal.com/
http://openid.aol.com/simonwillison
http://openid.aol.com/simonwillisonhttp://swillison.livejournal.com/http://simonwillison.net/http://openid.aol.com/simonwillisonhttp://openid.aol.com/simonwillisonhttp://swillison.livejournal.com/http://swillison.livejournal.com/http://simonwillison.net/http://simonwillison.net/http://simonwillison.myopenid.com/http://simonwillison.myopenid.com/7/30/2019 Advanced Django2929
67/81
How it works
You enter your OpenID on a site (instead of the
usual username and password)It redirects you back to your OpenID provider
They authenticate you in some way
They redirect you back to the original site
7/30/2019 Advanced Django2929
68/81
Simple registration(an optional but useful extension)
7/30/2019 Advanced Django2929
69/81
Your preferred username
Your e-mail address
Your first and last name
Your date of birth
Your language, country and timezone
Consumers can also ask...
7/30/2019 Advanced Django2929
70/81
How do you use
OpenID in a Djangoapplication?
7/30/2019 Advanced Django2929
71/81
Use the JanRain OpenID library
Pretty much a reference implementation for theOpenID spec
Well written, well tested but takes a while to getthe hang of
www.openidenabled.com/openid/libraries/python/
The hard way
http://www.openidenabled.com/openid/libraries/python/http://www.openidenabled.com/openid/libraries/python/http://www.openidenabled.com/openid/libraries/python/7/30/2019 Advanced Django2929
72/81
The easy way
Use django-openid
A simple wrapper around JanRain
A middleware component, some models and afew pre-written views
http://code.google.com/p/django-openid/
http://code.google.com/p/django-openid/http://code.google.com/p/django-openid/http://code.google.com/p/django-openid/7/30/2019 Advanced Django2929
73/81
Installation
Add 'django_openidconsumer' to yourINSTALLED_APPS setting
manage.py syncdb
Add the OpenIDMiddleware to yourMIDDLEWARE_CLASSES setting
Add the views to your URLconf
7/30/2019 Advanced Django2929
74/81
In urls.py
...
(r'^openid/$',
'django_openidconsumer.views.begin'),(r'^openid/complete/$','django_openidconsumer.views.complete'),
(r'^openid/signout/$',
'django_openidconsumer.views.signout'),...
7/30/2019 Advanced Django2929
75/81
request.openid
The middleware adds an openid property to theDjango request object
If the user is not signed in, this will be None
Otherwise, it will be an OpenID object; the str()
representation will be the OpenID (or userequest.openid.openid)
7/30/2019 Advanced Django2929
76/81
def example_view(request):if request.openid:
return HttpResponse("OpenID is %s" % escape(request.openid))else:
return HttpResponse("No OpenID")
7/30/2019 Advanced Django2929
77/81
The module supports users signing in with morethan one OpenID at a time
request.openids provides a list of allauthenticated OpenIDs
request.openid merely returns the mostrecent from this list
request.openids
F i l i i
7/30/2019 Advanced Django2929
78/81
(r'^openid/$', 'django_openidconsumer.views.begin', {
'sreg': 'email,nickname'}),
def example_sreg(request):if request.openid and request.openid.sreg.has_key('email'):
return HttpResponse("Your e-mail address is: %s" % escape(
request.openid.sreg['email']))
else:
return HttpResponse("No e-mail address")
For simple registration
7/30/2019 Advanced Django2929
79/81
django_openidauth, providing tools to associate
OpenIDs with existing django.contrib.authaccounts
django_openidserver, to make it easy to provide
OpenIDs for users of your Django application
Coming soon
7/30/2019 Advanced Django2929
80/81
More information
http://openid.net/
Also home to the OpenID mailing lists
http://www.openidenabled.com/
http://simonwillison.net/tags/openid/
http://code.google.com/p/django-openid/
http://code.google.com/p/django-openid/http://simonwillison.net/tags/openid/http://www.openidenabled.com/http://code.google.com/p/django-openid/http://code.google.com/p/django-openid/http://simonwillison.net/tags/openid/http://simonwillison.net/tags/openid/http://www.openidenabled.com/http://www.openidenabled.com/http://openid.net/http://openid.net/7/30/2019 Advanced Django2929
81/81
Thank you!