diff --git a/docs/tutorial02.txt b/docs/tutorial02.txt index 7552128285..93166e55d8 100644 --- a/docs/tutorial02.txt +++ b/docs/tutorial02.txt @@ -457,15 +457,8 @@ inclusion in the admin index template. It's a useful starting point. For full details on customizing the look and feel of the Django admin site in general, see the `Django admin CSS guide`_. +When you're comfortable with the admin site, read `part 3 of this tutorial`_ to +start working on public poll views. + .. _Django admin CSS guide: http://www.djangoproject.com/documentation/admin_css/ - -Coming soon -=========== - -The tutorial ends here for the time being. But check back within 48 hours for -the next installments: - -* Writing public-facing apps -* Using the cache framework -* Using the RSS framework -* Using the comments framework +.. _part 3 of this tutorial: http://www.djangoproject.com/documentation/tutorial3/ diff --git a/docs/tutorial03.txt b/docs/tutorial03.txt new file mode 100644 index 0000000000..ed5c7750d9 --- /dev/null +++ b/docs/tutorial03.txt @@ -0,0 +1,414 @@ +===================================== +Writing your first Django app, part 3 +===================================== + +By Adrian Holovaty + +This tutorial begins where `Tutorial 2`_ left off. We're continuing the Web-poll +application and will focus on creating the public interface -- "views." + +.. _Tutorial 2: http://www.djangoproject.com/documentation/tutorial2/ + +Philosophy +========== + +A view is a "type" of Web page in your Django application that generally serves +a specific function and has a specific template. For example, in a weblog +application, you might have the following views: + +* Blog homepage -- displays the latest few entries +* Entry "detail" page -- permalink page for a single entry +* Year-based archive page -- displays all months with entries in the given year +* Month-based archive page -- displays all days with entries in the given month +* Day-based archive page -- displays all entries in the given day +* Comment action -- handles posting comments to a given entry + +In our poll application, we'll have the following three views: + +* Poll "archive" page -- displays the latest few polls +* Poll "detail" page -- displays a poll question, with no results but with a form to vote +* Poll "results" page -- displays results for a particular poll +* Vote action -- handles voting for a particular choice in a particular poll + +In Django, each view is represented by a simple Python function. + +Design your URLs +================ + +The first step of writing views is to design your URL structure. You do this by +creating a Python module, called a URLconf. URLconfs are how Django associates +a given URL with given Python code. + +When a user requests a Django-powered page, the system looks at the +``ROOT_URLCONF`` setting, which contains a string in Python dotted syntax. +Django loads that module and looks for a module-level variable called +``urlpatterns``, which is a sequence of tuples in the following format:: + + (regular expression, Python callback function [, optional dictionary]) + +Django starts at the first regular expression and makes its way down the list, +comparing the requested URL against each regular expression until it finds one +that matches. + +When it finds a match, Django calls the Python callback function, with an +``HTTPRequest`` request as the first argument, any "captured" values from the +regular expression as keyword arguments, and, optionally, arbitrary keyword +arguments from the dictionary (an optional third item in the tuple). + +When you ran ``django-admin.py startproject myproject`` at the beginning of +Tutorial 1, it created a default URLconf in ``myproject/settings/urls/main.py``. +It also automatically set your ``ROOT_URLCONF`` setting to point at that file:: + + ROOT_URLCONF = 'myproject.settings.urls.main' + +Time for an example. Edit ``myproject/settings/urls/main.py`` so it looks like +this:: + + from django.conf.urls.defaults import * + + urlpatterns = patterns('', + (r'^polls/$', 'myproject.apps.polls.views.polls.index'), + (r'^polls/(?P\d+)/$', 'myproject.apps.polls.views.polls.detail'), + (r'^polls/(?P\d+)/results/$', 'myproject.apps.polls.views.polls.results'), + (r'^polls/(?P\d+)/vote/$', 'myproject.apps.polls.views.polls.vote'), + ) + +This is worth a review. When somebody requests a page from your Web site -- +say, "/polls/23/", Django will load this Python module, because it's pointed to +by the ``ROOT_URLCONF`` setting. It finds the variable named ``urlpatterns`` +and traverses the regular expressions. When it finds a regular expression that +matches -- ``r'^polls/(?P\d+)/$'`` -- it loads the associated Python +package/module: ``myproject.polls.views.polls.detail``. That corresponds to the +function ``detail()`` in ``myproject/polls/views/polls.py``. Finally, it calls +that ``detail()`` function like so:: + + detail(request=, poll_id=23) + +The ``poll_id=23`` part comes from ``(?P\d+)``. Using +``(?pattern)`` "captures" the text matched by ``pattern`` and sends it as +a keyword argument to the view function. + +Because the URL patterns are regular expressions, there really is no limit on +what you can do with them. And there's no need to add URL cruft such as +``.php`` -- unless you have a sick sense of humor, in which case you can do +something like this: + + (r'^polls/latest\.php$', 'myproject.apps.polls.views.polls.index'), + +But, don't do that. It's stupid. + +If you need help with regular expressions, see `Wikipedia's entry`_ and the +`Python documentation`_. Also, the O'Reilly book "Mastering Regular +Expressions" by Jeffrey Friedl is fantastic. + +Finally, a performance note: These regular expressions are compiled the first +time the URLconf module is loaded. They're super fast. + +.. _Wikipedia's entry: http://en.wikipedia.org/wiki/Regular_expression +.. _Python documentation: http://www.python.org/doc/current/lib/module-re.html + +Write your first view +===================== + +Well, we haven't created any views yet -- we just have the URLconf. But let's +make sure Django is following the URLconf properly. + +Set your ``DJANGO_SETTINGS_MODULE`` environment variable to your main settings +(``myproject.settings.main``), as we did with the admin settings in Tutorial 2. +Then, fire up the Django development Web server, as we also did in Tutorial 2:: + + django-admin.py runserver + +Now go to "http://localhost:8000/polls/" on your domain in your Web browser. +You should get a Python traceback with the following error message:: + + ViewDoesNotExist: myproject.polls.views.polls.index + +Try "/polls/23/", "/polls/23/results/" and "/polls/23/vote/". The error +messages should tell you which view Django tried (and failed to find, because +you haven't written any views yet). + +Time to write the first view. Create the file ``myproject/apps/polls/views/polls.py`` +and put the following Python code in it:: + + from django.utils.httpwrappers import HttpResponse + + def index(request): + return HttpResponse("Hello, world. You're at the poll index.") + +This is the simplest view possible. Restart your development server and go to +"/polls/". You should see your text. + +Now add the following view. It's slightly different, because it takes an +argument (which, remember, is passed in from whatever was captured by the +regular expression in the URLconf):: + + def detail(request, poll_id): + return HttpResponse("You're looking at poll %s." % poll_id) + +Take a look in your browser, at "/polls/34/". It'll display whatever ID you +provide in the URL. + +Write views that actually do something +====================================== + +Each view is responsible for doing one of two things: Returning an ``HttpResponse`` +object containing the content for the requested page, or raising an exception +such as ``Http404``. The rest is up to you. + +Your view can read records from a database, or not. It can use a template +system such as Django's -- or a third-party Python template system -- or not. +It can generate a PDF file, output XML, create a ZIP file on the fly, anything +you want, using whatever Python libraries you want. + +All Django wants is that ``HttpResponse``. Or an exception. + +Because it's convenient, let's use Django's own database API, which we covered +in Tutorial 1. Here's one stab at the ``index()`` view, which displays the +latest 5 poll questions in the system, separated by commas, according to +publication date:: + + from django.models.polls import polls + from django.utils.httpwrappers import HttpResponse + + def index(request): + latest_poll_list = polls.get_list(order_by=[('pub_date', 'DESC')], limit=5) + output = ', '.join([p.question for p in latest_poll_list]) + return HttpResponse(output) + +There's a problem here, though: The page's design is hard-coded in the view. If +you want to change the way the page looks, you'll have to edit this Python code. +So let's use Django's template system to separate the design from Python:: + + from django.core import template_loader + from django.core.extensions import DjangoContext as Context + from django.models.polls import polls + from django.utils.httpwrappers import HttpResponse + + def index(request): + latest_poll_list = polls.get_list(order_by=[('pub_date', 'DESC')], limit=5) + t = template_loader.get_template('polls/index') + c = Context(request, { + 'latest_poll_list': latest_poll_list, + }) + return HttpResponse(t.render(c)) + +That code loads the template called "polls/index" and passes it a context. The +context is a dictionary mapping template variable names to Python objects. + +Reload the page. Now you'll see an error: + + TemplateDoesNotExist: Your TEMPLATE_DIRS settings is empty. Change it to point to at least one template directory. + +Ah. There's no template yet. First, create a directory, somewhere on your +filesystem, whose contents Django can access. (Django runs as whatever user +your server runs.) Don't put them under your document root, though. You +probably shouldn't make them public, just for security's sake. + +Then edit ``TEMPLATE_DIRS`` in your ``main.py`` settings file to tell Django +where it can find templates -- just as you did in the "Customize the admin look +and feel" section of Tutorial 2. + +When you've done that, create a directory ``polls`` in your template directory. +Within that, create a file called ``index.html``. Django requires that +templates have ".html" extension. Note that our +``template_loader.get_template('polls/index')`` code from above maps to +"[template_directory]/polls/index.html" on the filesystem. + +Put the following code in that template:: + + {% if latest_poll_list %} +
    + {% for poll in latest_poll_list %} +
  • {{ poll.question }}
  • + {% endfor %} +
+ {% else %} +

No polls are available.

+ {% endif %} + +Templates are read from disk at each page request, so you don't have to restart +the server to see changes. Load the page in your Web browser, and you should +see a bulleted-list containing the "What's up" poll from Tutorial 1. + +Raising 404 +=========== + +Now, let's tackle the poll detail view -- the page that displays the question +for a given poll. Here's the view:: + + from django.core.exceptions import Http404 + def detail(request, poll_id): + try: + p = polls.get_object(id__exact=poll_id) + except polls.PollDoesNotExist: + raise Http404 + t = template_loader.get_template('polls/detail') + c = Context(request, { + 'poll': p, + }) + return HttpResponse(t.render(c)) + +The new concept here: The view raises the ``django.core.exceptions.Http404`` +exception if a poll with the requested ID doesn't exist. + +Write a 404 (page not found) view +================================= + +When you raise ``Http404`` from within a view, Django will load a special view +devoted to handling 404 errors. It finds it by looking for the variable +``handler404``, which is a string in Python dotted syntax -- the same format +the normal URLconf callbacks use. A 404 view itself has nothing special: It's +just a normal view. + +You normally won't have to bother with writing 404 views. By default, URLconfs +have the following line up top:: + + from django.conf.urls.defaults import * + +That takes care of setting ``handler404`` in the current module. As you can see +in ``django/conf/urls/defaults.py``, ``handler404`` is set to +``'django.views.defaults.page_not_found'`` by default. + +Two more things to note about 404 views: + +* The 404 view is also called if Django doesn't find a match after checking + every regular expression in the URLconf. +* If you don't define your own 404 view -- and simply use the default, which is + recommended -- you still have one obligation: To create a ``404.html`` + template in the root of your template directory. The default 404 view will + use that template for all 404 errors. + +Write a 500 (server error) view +=============================== + +Similarly, URLconfs may define a ``handler500``, which points to a view to call +in case of server errors. Server errors happen when you have runtime errors in +view code. + +Use the template system +======================= + +Back to our ``polls.detail`` view. Given the context variable ``poll``, here's +what the template might look like:: + +

{{ poll.question }}

+
    + {% for choice in poll.get_choice_list %} +
  • {{ choice.choice }}
  • + {% endfor %} +
+ +The template system uses dot-lookup syntax to access variable attributes. In +the example of ``{{ poll.question }}``, first Django does a dictionary lookup +on the object ``poll``. Failing that, it tries attribute lookup -- which works, +in this case. If attribute lookup had failed, it would've tried calling the +method ``choice()`` on the poll object. + +Method-calling happens in the ``{% for %}`` loop: ``poll.get_choice_list`` is +interpreted as the Python code ``poll.get_choice_list()``, which returns a list +of Choice objects and is suitable for iteration via the ``{% for %}`` tag. + +See the `template guide`_ for full details on how templates work. + +.. _template guide: http://www.djangoproject.com/documentation/templates/ + +Simplifying the URLconfs +======================== + +Take some time to play around with the views and template system. As you edit +the URLconf, you may notice there's a fair bit of redundancy in it:: + + urlpatterns = patterns('', + (r'^polls/$', 'myproject.apps.polls.views.polls.index'), + (r'^polls/(?P\d+)/$', 'myproject.apps.polls.views.polls.detail'), + (r'^polls/(?P\d+)/results/$', 'myproject.apps.polls.views.polls.results'), + (r'^polls/(?P\d+)/vote/$', 'myproject.apps.polls.views.polls.vote'), + ) + +Namely, ``myproject.apps.polls.views.polls`` is in every callback. + +Because this is a common case, the URLconf framework provides a shortcut for +common prefixes. You can factor out the common prefixes and add them as the +first argument to ``patterns()``, like so:: + + urlpatterns = patterns('myproject.apps.polls.views.polls', + (r'^polls/$', 'index'), + (r'^polls/(?P\d+)/$', 'detail'), + (r'^polls/(?P\d+)/results/$', 'results'), + (r'^polls/(?P\d+)/vote/$', 'vote'), + ) + +This is functionally identical to the previous formatting. It's just a bit +tidier. + +Decoupling the URLconfs +======================= + +While we're at it, we should take the time to decouple our poll-app URLs from +our Django project configuration. Django apps are meant to be pluggable -- that +is, each particular app should be transferrable to another Django installation +with minimal fuss. + +Our poll app is pretty decoupled at this point, thanks to the strict directory +structure that ``django-admin.py startapp`` created, but one part of it is +coupled to the Django settings: The URLconf. + +We've been editing the URLs in ``myproject/settings/urls/main.py``, but the +URL design of an app is specific to the app, not to the Django installation -- +so let's move the URLs within the app directory. + +Just copy the file ``myproject/settings/urls/main.py`` to +``myproject/apps/polls/urls/polls.py``, which had already been created, as a +stub, by ``django-admin.py startapp``. + +Then, change ``myproject/settings/urls/main.py`` to remove the poll-specific +URLs and insert an ``include()``:: + + (r'^polls/', include('myproject.apps.polls.urls.polls')), + +Notes: + +``include()``, simply, references another URLconf. Note that the regular +expression doesn't have a ``$`` (end-of-string match character) but has the +trailing slash. Whenever Django encounters ``include()``, it chops off whatever +part of the URL matched up to that point and sends the remaining string to the +included URLconf for further processing. + +Here's what happens if a user goes to "/polls/34/" in this system: + +* Django will find the match at ``'^polls/'`` +* It will strip off the matching text (``"polls/"``) and send the remaining + text -- ``"34/"`` -- to the 'myproject.apps.polls.urls.polls' urlconf for + further processing. + +Now that we've decoupled that, we need to decouple the +'myproject.apps.polls.urls.polls' urlconf by removing the leading "polls/" +from each line:: + + urlpatterns = patterns('myproject.apps.polls.views.polls', + (r'^$', 'index'), + (r'^(?P\d+)/$', 'detail'), + (r'^(?P\d+)/results/$', 'results'), + (r'^(?P\d+)/vote/$', 'vote'), + ) + +The idea behind ``include()`` and URLconf decoupling is to make it easy to +plug-and-play URLs. Now that polls are in their own URLconf, they can be placed +under "/polls/", or under "/fun_polls/", or under "/content/polls/", or any +other URL root, and the app will still work. + +All the poll app cares about is its relative URLs, not its absolute URLs. + +Coming soon +=========== + +The tutorial ends here for the time being. But check back within 48 hours for +the next installments: + +* Advanced view features: Form processing +* Using the RSS framework +* Using the cache framework +* Using the comments framework +* Advanced admin features: Permissions +* Advanced admin features: Custom JavaScript