Boston Python Workshop/Saturday/Web app project: Difference between revisions

imported>Paulproteus
imported>Paulproteus
Line 501:
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.templateshortcuts import Context, loaderrender_to_response
from polls.models import Poll
from django.http import HttpResponse
 
def index(request):
latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
context = {'latest_poll_list': latest_poll_list}
t = loader.get_template('polls/index.html')
return render_to_response('polls/index.html', context)
c = Context({
'latest_poll_list': latest_poll_list,
})
return HttpResponse(t.render(c))
 
To recap what this does:
That code loads the template called "polls/index.html" and passes it a context. The context is a dictionary mapping template variable names to Python objects.
 
* Creates a variable called ''latest_poll_list''. Django queries the database for ''all'' Poll objects, ordered by ''pub_date'' with most recent first, and uses ''slicing'' to get the first five.
* Creates a variable called ''context'' that is a dictionary with one key.
* Evaluates the ''render_to_response'' function with two arguments, and returns whatever that returns.
 
''render_to_response'' loads the template called "polls/index.html" and passes it a value as ''context''. The context is a dictionary mapping template variable names to Python objects.
 
If you can read this this ''view'' function without being overwhelmed, then you understand the basics of Django views. Now is a good time to reflect and make sure you do. (If you have questions, ask a volunteer for help.)
 
Reload the page. Now you'll see an error:
Line 520 ⟶ 524:
polls/index.html
 
Ah. There's no template yet. Let's make one.
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 settings.py to tell Django where it can find templates -- just as you did in the "Customize the admin look and feel" section of Tutorial 2.
 
First, let's make a directory where templates will live. We'll need a templates directory right alongside the ''views.py'' for the ''polls'' app. This is what I would do:
When you've done that, create a directory polls in your template directory. Within that, create a file called index.html. Note that our loader.get_template('polls/index.html') code from above maps to "[template_directory]/polls/index.html" on the filesystem.
 
mkdir -p polls/templates/polls
 
Then create a file in there called ''index.html''. Within that, create a file called index.html. Note that our loader.get_template('polls/index.html') code from above maps to "[template_directory]/polls/index.html" on the filesystem.
 
Put the following code in that template:
 
{% if latest_poll_list %}
<ul>
{% for poll in latest_poll_list %}
<li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
 
Load the page in your Web browser, and you should see a bulleted-list containing the "What's up" poll from Tutorial 1. The link points to the poll's detail page.
A shortcut: render_to_response()¶
 
=== Raising 404 ===
It's a very common idiom to load a template, fill a context and return an HttpResponse object with the result of the rendered template. Django provides a shortcut. Here's the full index() view, rewritten:
 
Now, let's tackle the poll detail view -- the page that displays the question for a given poll. This view uses Python ''exceptions'':
from django.shortcuts import render_to_response
from polls.models import Poll
 
from django.http import Http404
def index(request):
# ...
latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
def detail(request, poll_id):
return render_to_response('polls/index.html', {'latest_poll_list': latest_poll_list})
try:
 
p = Poll.objects.get(pk=poll_id)
Note that once we've done this in all these views, we no longer need to import loader, Context and HttpResponse.
except Poll.DoesNotExist:
 
raise Http404
The render_to_response() function takes a template name as its first argument and a dictionary as its optional second argument. It returns an HttpResponse object of the given template rendered with the given context.
return render_to_response('polls/detail.html', {'poll': p})
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.http import Http404
# ...
def detail(request, poll_id):
try:
p = Poll.objects.get(pk=poll_id)
except Poll.DoesNotExist:
raise Http404
return render_to_response('polls/detail.html', {'poll': p})
 
The new concept here: The view raises the Http404 exception if a poll with the requested ID doesn't exist.
 
We'll discuss what you could put in that polls/detail.html template a bit later, but ifIf you'd like to quickly get the above example working, just:
 
{{ poll }}
 
will get you started for now.
A shortcut: get_object_or_404()¶
 
It's a very common idiom to use get() and raise Http404 if the object doesn't exist. Django provides a shortcut. Here's the detail() view, rewritten:
 
from django.shortcuts import render_to_response, get_object_or_404
# ...
def detail(request, poll_id):
p = get_object_or_404(Poll, pk=poll_id)
return render_to_response('polls/detail.html', {'poll': p})
 
The get_object_or_404() function takes a Django model as its first argument and an arbitrary number of keyword arguments, which it passes to the module's get() function. It raises Http404 if the object doesn't exist.
 
Philosophy
 
Why do we use a helper function get_object_or_404() instead of automatically catching the ObjectDoesNotExist exceptions at a higher level, or having the model API raise Http404 instead of ObjectDoesNotExist?
 
Because that would couple the model layer to the view layer. One of the foremost design goals of Django is to maintain loose coupling.
 
There's also a get_list_or_404() function, which works just as get_object_or_404() -- except using filter() instead of get(). It raises Http404 if the list is empty.
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.
 
Four more things to note about 404 views:
 
* If DEBUG is set to True (in your settings module) then your 404 view will never be used (and thus the 404.html template will never be rendered) because the traceback will be displayed instead.
* 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.
* If DEBUG is set to False (in your settings module) and if you didn't create a 404.html file, an Http500 is raised instead. So remember to create a 404.html.
 
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 the detail() view for our poll application. Given the context variable poll, here's what the "polls/detail.html" template might look like:
 
<h1>{{ poll.question }}</h1>
<ul>
{% for choice in poll.choice_set.all %}
<li>{{ choice.choice }}</li>
{% endfor %}
</ul>
 
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 an attribute lookup -- which works, in this case. If attribute lookup had failed, it would've tried a list-index lookup.
 
Method-calling happens in the {% for %} loop: poll.choice_set.all is interpreted as the Python code poll.choice_set.all(), which returns an iterable of Choice objects and is suitable for use in the {% for %} tag.
 
See the template guide for more about 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/$', 'polls.views.index'),
(r'^polls/(?P<poll_id>\d+)/$', 'polls.views.detail'),
(r'^polls/(?P<poll_id>\d+)/results/$', 'polls.views.results'),
(r'^polls/(?P<poll_id>\d+)/vote/$', 'polls.views.vote'),
)
 
Namely, polls.views 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('polls.views',
(r'^polls/$', 'index'),
(r'^polls/(?P<poll_id>\d+)/$', 'detail'),
(r'^polls/(?P<poll_id>\d+)/results/$', 'results'),
(r'^polls/(?P<poll_id>\d+)/vote/$', 'vote'),
)
 
This is functionally identical to the previous formatting. It's just a bit tidier.
 
Since you generally don't want the prefix for one app to be applied to every callback in your URLconf, you can concatenate multiple patterns(). Your full mysite/urls.py might now look like this:
 
from django.conf.urls.defaults import *
 
from django.contrib import admin
admin.autodiscover()
 
urlpatterns = patterns('polls.views',
(r'^polls/$', 'index'),
(r'^polls/(?P<poll_id>\d+)/$', 'detail'),
(r'^polls/(?P<poll_id>\d+)/results/$', 'results'),
(r'^polls/(?P<poll_id>\d+)/vote/$', 'vote'),
)
 
urlpatterns += patterns('',
(r'^admin/', include(admin.site.urls)),
)
 
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 transferable to another Django installation with minimal fuss.
 
Our poll app is pretty decoupled at this point, thanks to the strict directory structure that python manage.py startapp created, but one part of it is coupled to the Django settings: The URLconf.
 
We've been editing the URLs in mysite/urls.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.
 
Copy the file mysite/urls.py to polls/urls.py. Then, change mysite/urls.py to remove the poll-specific URLs and insert an include(), leaving you with:
 
# This also imports the include function
from django.conf.urls.defaults import *
 
Add it to a new template file that you create, ''detail.html''.
from django.contrib import admin
admin.autodiscover()
 
Does your detail view work? Try it: http://127.0.0.1:8000/polls/detail/1/
urlpatterns = patterns('',
(r'^polls/', include('polls.urls')),
(r'^admin/', include(admin.site.urls)),
)
 
=== Adding more detail ===
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.
 
Let's give the detail view some more '''detail'''.
Here's what happens if a user goes to "/polls/34/" in this system:
 
We pass in a variable called '''poll''' that points to an instance of the Poll class. So you can pull out more information by writing this into the "polls/detail.html" template:
* Django will find the match at '^polls/'
* Then, Django will strip off the matching text ("polls/") and send the remaining text -- "34/" -- to the 'polls.urls' URLconf for further processing.
 
<h1>{{ poll.question }}</h1>
Now that we've decoupled that, we need to decouple the polls.urls URLconf by removing the leading "polls/" from each line, and removing the lines registering the admin site. Your polls.urls file should now look like this:
<ul>
{% for choice in poll.choice_set.all %}
<li>{{ choice.choice }}</li>
{% endfor %}
</ul>
 
The template system uses dot-lookup syntax to access variable attributes. Django's template language is a bit sloppy: in pure Python, the '''.''' (dot) only lets you get attributes from objects. In this example, we are just doing attribute lookup, but in general if you're not sure how to get data out of an object in Django, try '''dot'''.
from django.conf.urls.defaults import *
 
Method-calling happens in the {% for %} loop: poll.choice_set.all is interpreted as the Python code poll.choice_set.all(), which returns a sequence of Choice objects and is suitable for use in the {% for %} tag.
urlpatterns = patterns('polls.views',
(r'^$', 'index'),
(r'^(?P<poll_id>\d+)/$', 'detail'),
(r'^(?P<poll_id>\d+)/results/$', 'results'),
(r'^(?P<poll_id>\d+)/vote/$', 'vote'),
)
 
=== Adding some style ===
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 path root, and the app will still work.
 
The web page looks okay, but it is somewhat drab.
All the poll app cares about is its relative path, not its absolute path.
 
FIXME: CSS
When you're comfortable with writing views, read part 4 of this tutorial to learn about simple form processing and generic views.
 
== Part 2.5: Deploy your web app! ==
Anonymous user