+ All Categories
Home > Documents > Advanced Django2929

Advanced Django2929

Date post: 14-Apr-2018
Category:
Upload: walter-angolar-da-silva
View: 215 times
Download: 0 times
Share this document with a friend

of 81

Transcript
  • 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!


Recommended