Fixed #20910 -- Added a "snippet" sphinx directive to allow prefixing a filename.

Thanks Marc Tamlyn for the suggestion.
This commit is contained in:
M Nasimul Haque 2013-09-23 23:23:47 +01:00 committed by Tim Graham
parent e077224f4a
commit d07d6ae116
10 changed files with 403 additions and 124 deletions

View File

@ -282,6 +282,7 @@ answer newbie questions, and generally made Django that much better:
Scot Hacker <shacker@birdhouse.org>
dAniel hAhler
hambaloney
Nasimul Haque <nasim.haque@gmail.com>
Will Hardy <django@willhardy.com.au>
Brian Harring <ferringb@gmail.com>
Brant Harris

View File

@ -5,11 +5,15 @@ import json
import os
import re
from docutils import nodes
from docutils.parsers.rst import directives
from sphinx import addnodes, __version__ as sphinx_ver
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.writers.html import SmartyPantsHTMLTranslator
from sphinx.util.console import bold
from sphinx.util.compat import Directive
from sphinx.util.nodes import set_source_info
# RE for option descriptions without a '--' prefix
simple_option_desc_re = re.compile(
@ -53,6 +57,136 @@ def setup(app):
app.add_directive('versionchanged', VersionDirective)
app.add_builder(DjangoStandaloneHTMLBuilder)
# register the snippet directive
app.add_directive('snippet', SnippetWithFilename)
# register a node for snippet directive so that the xml parser
# knows how to handle the enter/exit parsing event
app.add_node(snippet_with_filename,
html=(visit_snippet, depart_snippet_literal),
latex=(visit_snippet_latex, depart_snippet_latex),
man=(visit_snippet_literal, depart_snippet_literal),
text=(visit_snippet_literal, depart_snippet_literal),
texinfo=(visit_snippet_literal, depart_snippet_literal))
class snippet_with_filename(nodes.literal_block):
"""
Subclass the literal_block to override the visit/depart event handlers
"""
pass
def visit_snippet_literal(self, node):
"""
default literal block handler
"""
self.visit_literal_block(node)
def depart_snippet_literal(self, node):
"""
default literal block handler
"""
self.depart_literal_block(node)
def visit_snippet(self, node):
"""
HTML document generator visit handler
"""
lang = self.highlightlang
linenos = node.rawsource.count('\n') >= self.highlightlinenothreshold - 1
fname = node['filename']
highlight_args = node.get('highlight_args', {})
if node.has_key('language'):
# code-block directives
lang = node['language']
highlight_args['force'] = True
if node.has_key('linenos'):
linenos = node['linenos']
def warner(msg):
self.builder.warn(msg, (self.builder.current_docname, node.line))
highlighted = self.highlighter.highlight_block(node.rawsource, lang,
warn=warner,
linenos=linenos,
**highlight_args)
starttag = self.starttag(node, 'div', suffix='',
CLASS='highlight-%s' % lang)
self.body.append(starttag)
self.body.append('<div class="snippet-filename">%s</div>\n''' % (fname,))
self.body.append(highlighted)
self.body.append('</div>\n')
raise nodes.SkipNode
def visit_snippet_latex(self, node):
"""
Latex document generator visit handler
"""
self.verbatim = ''
def depart_snippet_latex(self, node):
"""
Latex document generator depart handler.
"""
code = self.verbatim.rstrip('\n')
lang = self.hlsettingstack[-1][0]
linenos = code.count('\n') >= self.hlsettingstack[-1][1] - 1
fname = node['filename']
highlight_args = node.get('highlight_args', {})
if 'language' in node:
# code-block directives
lang = node['language']
highlight_args['force'] = True
if 'linenos' in node:
linenos = node['linenos']
def warner(msg):
self.builder.warn(msg, (self.curfilestack[-1], node.line))
hlcode = self.highlighter.highlight_block(code, lang, warn=warner,
linenos=linenos,
**highlight_args)
self.body.append('\n{\\colorbox[rgb]{0.9,0.9,0.9}'
'{\\makebox[\\textwidth][l]'
'{\\small\\texttt{%s}}}}\n' % (fname,))
if self.table:
hlcode = hlcode.replace('\\begin{Verbatim}',
'\\begin{OriginalVerbatim}')
self.table.has_problematic = True
self.table.has_verbatim = True
hlcode = hlcode.rstrip()[:-14] # strip \end{Verbatim}
hlcode = hlcode.rstrip() + '\n'
self.body.append('\n' + hlcode + '\\end{%sVerbatim}\n' %
(self.table and 'Original' or ''))
self.verbatim = None
class SnippetWithFilename(Directive):
"""
The 'snippet' directive that allows to add the filename (optional)
of a code snippet in the document. This is modeled after CodeBlock.
"""
has_content = True
optional_arguments = 1
option_spec = {'filename': directives.unchanged_required}
def run(self):
code = u'\n'.join(self.content)
literal = snippet_with_filename(code, code)
if self.arguments:
literal['language'] = self.arguments[0]
literal['filename'] = self.options['filename']
set_source_info(self, literal)
return [literal]
class VersionDirective(Directive):
has_content = True

View File

@ -100,6 +100,9 @@ pre { font-size:small; background:#E0FFB8; border:1px solid #94da3a; border-widt
dt .literal, table .literal { background:none; }
#bd a.reference { text-decoration: none; }
#bd a.reference tt.literal { border-bottom: 1px #234f32 dotted; }
div.snippet-filename { color: white; background-color: #234F32; margin: 0; padding: 2px 5px; width: 100%; font-family: monospace; font-size: small; line-height: 1.3em; }
div.snippet-filename + div.highlight > pre { margin-top: 0; }
div.snippet-filename + pre { margin-top: 0; }
/* Restore colors of pygments hyperlinked code */
#bd .highlight .k a:link, #bd .highlight .k a:visited { color: #000000; text-decoration: none; border-bottom: 1px dotted #000000; }

View File

@ -120,7 +120,7 @@ this. For a small app like polls, this process isn't too difficult.
1. First, create a parent directory for ``polls``, outside of your Django
project. Call this directory ``django-polls``.
.. admonition:: Choosing a name for your app
.. admonition:: Choosing a name for your app
When choosing a name for your package, check resources like PyPI to avoid
naming conflicts with existing packages. It's often useful to prepend
@ -130,7 +130,10 @@ this. For a small app like polls, this process isn't too difficult.
2. Move the ``polls`` directory into the ``django-polls`` directory.
3. Create a file ``django-polls/README.rst`` with the following contents::
3. Create a file ``django-polls/README.rst`` with the following contents:
.. snippet::
:filename: django-polls/README.rst
=====
Polls
@ -163,17 +166,21 @@ this. For a small app like polls, this process isn't too difficult.
5. Visit http://127.0.0.1:8000/polls/ to participate in the poll.
4. Create a ``django-polls/LICENSE`` file. Choosing a license is beyond the
scope of this tutorial, but suffice it to say that code released publicly
without a license is *useless*. Django and many Django-compatible apps are
distributed under the BSD license; however, you're free to pick your own
license. Just be aware that your licensing choice will affect who is able
to use your code.
scope of this tutorial, but suffice it to say that code released publicly
without a license is *useless*. Django and many Django-compatible apps are
distributed under the BSD license; however, you're free to pick your own
license. Just be aware that your licensing choice will affect who is able
to use your code.
5. Next we'll create a ``setup.py`` file which provides details about how to
build and install the app. A full explanation of this file is beyond the
scope of this tutorial, but the `distribute docs
<http://packages.python.org/distribute/setuptools.html>`_ have a good explanation.
Create a file ``django-polls/setup.py`` with the following contents::
build and install the app. A full explanation of this file is beyond the
scope of this tutorial, but the `distribute docs
<http://packages.python.org/distribute/setuptools.html>`_ have a good
explanation. Create a file ``django-polls/setup.py`` with the following
contents:
.. snippet::
:filename: django-polls/setup.py
import os
from setuptools import setup
@ -209,7 +216,7 @@ Create a file ``django-polls/setup.py`` with the following contents::
],
)
.. admonition:: I thought you said we were going to use ``distribute``?
.. admonition:: I thought you said we were going to use ``distribute``?
Distribute is a drop-in replacement for ``setuptools``. Even though we
appear to import from ``setuptools``, since we have ``distribute``
@ -220,7 +227,10 @@ Create a file ``django-polls/setup.py`` with the following contents::
distribute docs referred to in the previous step discuss this file in more
details. To include the templates, the ``README.rst`` and our ``LICENSE``
file, create a file ``django-polls/MANIFEST.in`` with the following
contents::
contents:
.. snippet::
:filename: django-polls/MANIFEST.in
include LICENSE
include README.rst

View File

@ -344,7 +344,10 @@ the text of the choice and a vote tally. Each ``Choice`` is associated with a
``Question``.
These concepts are represented by simple Python classes. Edit the
:file:`polls/models.py` file so it looks like this::
:file:`polls/models.py` file so it looks like this:
.. snippet::
:filename: polls/models.py
from django.db import models
@ -415,7 +418,10 @@ But first we need to tell our project that the ``polls`` app is installed.
Edit the :file:`mysite/settings.py` file again, and change the
:setting:`INSTALLED_APPS` setting to include the string ``'polls'``. So it'll
look like this::
look like this:
.. snippet::
:filename: mysite/settings.py
INSTALLED_APPS = (
'django.contrib.admin',
@ -589,7 +595,10 @@ of this object. Let's fix that by editing the ``Question`` model (in the
``polls/models.py`` file) and adding a
:meth:`~django.db.models.Model.__unicode__` method to both ``Question`` and
``Choice``. On Python 3, simply replace ``__unicode__`` by ``__str__`` in the
following example::
following example:
.. snippet::
:filename: polls/models.py
from django.db import models
@ -633,7 +642,10 @@ admin.
luck, things should Just Work for you.
Note these are normal Python methods. Let's add a custom method, just for
demonstration::
demonstration:
.. snippet::
:filename: polls/models.py
import datetime
from django.utils import timezone

View File

@ -79,7 +79,10 @@ But where's our poll app? It's not displayed on the admin index page.
Just one thing to do: we need to tell the admin that ``Question``
objects have an admin interface. To do this, open the :file:`polls/admin.py`
file, and edit it to look like this::
file, and edit it to look like this:
.. snippet::
:filename: polls/admin.py
from django.contrib import admin
from polls.models import Question
@ -156,7 +159,10 @@ to customize how the admin form looks and works. You'll do this by telling
Django the options you want when you register the object.
Let's see how this works by re-ordering the fields on the edit form. Replace
the ``admin.site.register(Question)`` line with::
the ``admin.site.register(Question)`` line with:
.. snippet::
:filename: polls/admin.py
from django.contrib import admin
from polls.models import Question
@ -181,7 +187,10 @@ This isn't impressive with only two fields, but for admin forms with dozens
of fields, choosing an intuitive order is an important usability detail.
And speaking of forms with dozens of fields, you might want to split the form
up into fieldsets::
up into fieldsets:
.. snippet::
:filename: polls/admin.py
from django.contrib import admin
from polls.models import Question
@ -204,7 +213,10 @@ Here's what our form looks like now:
You can assign arbitrary HTML classes to each fieldset. Django provides a
``"collapse"`` class that displays a particular fieldset initially collapsed.
This is useful when you have a long form that contains a number of fields that
aren't commonly used::
aren't commonly used:
.. snippet::
:filename: polls/admin.py
from django.contrib import admin
from polls.models import Question
@ -228,7 +240,10 @@ the admin page doesn't display choices.
Yet.
There are two ways to solve this problem. The first is to register ``Choice``
with the admin just as we did with ``Question``. That's easy::
with the admin just as we did with ``Question``. That's easy:
.. snippet::
:filename: polls/admin.py
from django.contrib import admin
from polls.models import Choice
@ -258,7 +273,10 @@ It'd be better if you could add a bunch of Choices directly when you create the
``Question`` object. Let's make that happen.
Remove the ``register()`` call for the ``Choice`` model. Then, edit the ``Question``
registration code to read::
registration code to read:
.. snippet::
:filename: polls/admin.py
from django.contrib import admin
from polls.models import Choice, Question
@ -302,7 +320,10 @@ that you can't remove the original three slots. This image shows an added slot:
One small problem, though. It takes a lot of screen space to display all the
fields for entering related ``Choice`` objects. For that reason, Django offers a
tabular way of displaying inline related objects; you just need to change
the ``ChoiceInline`` declaration to read::
the ``ChoiceInline`` declaration to read:
.. snippet::
:filename: polls/admin.py
class ChoiceInline(admin.TabularInline):
#...
@ -330,14 +351,20 @@ Here's what it looks like at this point:
By default, Django displays the ``str()`` of each object. But sometimes it'd be
more helpful if we could display individual fields. To do that, use the
``list_display`` admin option, which is a tuple of field names to display, as
columns, on the change list page for the object::
columns, on the change list page for the object:
.. snippet::
:filename: polls/admin.py
class QuestionAdmin(admin.ModelAdmin):
# ...
list_display = ('question_text', 'pub_date')
Just for good measure, let's also include the ``was_published_recently`` custom
method from Tutorial 1::
method from Tutorial 1:
.. snippet::
:filename: polls/admin.py
class QuestionAdmin(admin.ModelAdmin):
# ...
@ -356,7 +383,10 @@ underscores replaced with spaces), and that each line contains the string
representation of the output.
You can improve that by giving that method (in :file:`polls/models.py`) a few
attributes, as follows::
attributes, as follows:
.. snippet::
:filename: polls/admin.py
class Question(models.Model):
# ...
@ -417,7 +447,10 @@ whatever user your server runs.) However, keeping your templates within the
project is a good convention to follow.
Open your settings file (:file:`mysite/settings.py`, remember) and add a
:setting:`TEMPLATE_DIRS` setting::
:setting:`TEMPLATE_DIRS` setting:
.. snippet::
:filename: mysite/settings.py
TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')]

View File

@ -84,7 +84,10 @@ Your app directory should now look like::
urls.py
views.py
In the ``polls/urls.py`` file include the following code::
In the ``polls/urls.py`` file include the following code:
.. snippet::
:filename: polls/urls.py
from django.conf.urls import patterns, url
@ -96,7 +99,10 @@ In the ``polls/urls.py`` file include the following code::
The next step is to point the root URLconf at the ``polls.urls`` module. In
``mysite/urls.py`` insert an :func:`~django.conf.urls.include`, leaving you
with::
with:
.. snippet::
:filename: mysite/urls.py
from django.conf.urls import patterns, include, url
@ -172,7 +178,10 @@ Writing more views
==================
Now let's add a few more views to ``polls/views.py``. These views are
slightly different, because they take an argument::
slightly different, because they take an argument:
.. snippet::
:filename: polls/views.py
def detail(request, question_id):
return HttpResponse("You're looking at question %s." % question_id)
@ -185,7 +194,10 @@ slightly different, because they take an argument::
return HttpResponse("You're voting on question %s." % question_id)
Wire these new views into the ``polls.urls`` module by adding the following
:func:`~django.conf.urls.url` calls::
:func:`~django.conf.urls.url` calls:
.. snippet::
:filename: polls/urls.py
from django.conf.urls import patterns, url
@ -269,7 +281,10 @@ All Django wants is that :class:`~django.http.HttpResponse`. Or an exception.
Because it's convenient, let's use Django's own database API, which we covered
in :doc:`Tutorial 1 </intro/tutorial01>`. 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::
commas, according to publication date:
.. snippet::
:filename: polls/views.py
from django.http import HttpResponse
@ -327,7 +342,8 @@ Django simply as ``polls/index.html``.
Put the following code in that template:
.. code-block:: html+django
.. snippet:: html+django
:filename: polls/templates/polls/index.html
{% if latest_question_list %}
<ul>
@ -339,7 +355,10 @@ Put the following code in that template:
<p>No polls are available.</p>
{% endif %}
Now let's update our ``index`` view in ``polls/views.py`` to use the template::
Now let's update our ``index`` view in ``polls/views.py`` to use the template:
.. snippet::
:filename: polls/views.py
from django.http import HttpResponse
from django.template import RequestContext, loader
@ -369,7 +388,10 @@ A shortcut: :func:`~django.shortcuts.render`
It's a very common idiom to load a template, fill a context and return an
:class:`~django.http.HttpResponse` object with the result of the rendered
template. Django provides a shortcut. Here's the full ``index()`` view,
rewritten::
rewritten:
.. snippet::
:filename: polls/views.py
from django.shortcuts import render
@ -395,7 +417,10 @@ Raising a 404 error
===================
Now, let's tackle the question detail view -- the page that displays the question text
for a given poll. Here's the view::
for a given poll. Here's the view:
.. snippet::
:filename: polls/views.py
from django.http import Http404
from django.shortcuts import render
@ -414,7 +439,10 @@ if a question with the requested ID doesn't exist.
We'll discuss what you could put in that ``polls/detail.html`` template a bit
later, but if you'd like to quickly get the above example working, a file
containing just::
containing just:
.. snippet:: html+django
:filename: polls/templates/polls/detail.html
{{ question }}
@ -425,7 +453,10 @@ A shortcut: :func:`~django.shortcuts.get_object_or_404`
It's a very common idiom to use :meth:`~django.db.models.query.QuerySet.get`
and raise :exc:`~django.http.Http404` if the object doesn't exist. Django
provides a shortcut. Here's the ``detail()`` view, rewritten::
provides a shortcut. Here's the ``detail()`` view, rewritten:
.. snippet::
:filename: polls/views.py
from django.shortcuts import render, get_object_or_404
@ -466,7 +497,8 @@ Back to the ``detail()`` view for our poll application. Given the context
variable ``question``, here's what the ``polls/detail.html`` template might look
like:
.. code-block:: html+django
.. snippet:: html+django
:filename: polls/templates/polls/detail.html
<h1>{{ question.question_text }}</h1>
<ul>
@ -549,7 +581,10 @@ make it so that Django knows which app view to create for a url when using the
The answer is to add namespaces to your root URLconf. In the ``mysite/urls.py``
file (the project's ``urls.py``, not the application's), go ahead and change
it to include namespacing::
it to include namespacing:
.. snippet::
:filename: mysite/urls.py
from django.conf.urls import patterns, include, url
@ -563,13 +598,15 @@ it to include namespacing::
Now change your ``polls/index.html`` template from:
.. code-block:: html+django
.. snippet:: html+django
:filename: polls/templates/polls/index.html
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
to point at the namespaced detail view:
.. code-block:: html+django
.. snippet:: html+django
:filename: polls/templates/polls/index.html
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

View File

@ -12,7 +12,8 @@ Write a simple form
Let's update our poll detail template ("polls/detail.html") from the last
tutorial, so that the template contains an HTML ``<form>`` element:
.. code-block:: html+django
.. snippet:: html+django
:filename: polls/templates/polls/detail.html
<h1>{{ question.question_text }}</h1>
@ -54,12 +55,18 @@ A quick rundown:
Now, let's create a Django view that handles the submitted data and does
something with it. Remember, in :doc:`Tutorial 3 </intro/tutorial03>`, we
created a URLconf for the polls application that includes this line::
created a URLconf for the polls application that includes this line:
.. snippet::
:filename: polls/urls.py
url(r'^(?P<question_id>\d+)/vote/$', views.vote, name='vote'),
We also created a dummy implementation of the ``vote()`` function. Let's
create a real version. Add the following to ``polls/views.py``::
create a real version. Add the following to ``polls/views.py``:
.. snippet::
:filename: polls/views.py
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
@ -134,7 +141,10 @@ object. For more on :class:`~django.http.HttpRequest` objects, see the
:doc:`request and response documentation </ref/request-response>`.
After somebody votes in a question, the ``vote()`` view redirects to the results
page for the question. Let's write that view::
page for the question. Let's write that view:
.. snippet::
:filename: polls/views.py
from django.shortcuts import get_object_or_404, render
@ -149,7 +159,8 @@ redundancy later.
Now, create a ``polls/results.html`` template:
.. code-block:: html+django
.. snippet:: html+django
:filename: polls/templates/polls/results.html
<h1>{{ question.question_text }}</h1>
@ -205,7 +216,10 @@ Read on for details.
Amend URLconf
-------------
First, open the ``polls/urls.py`` URLconf and change it like so::
First, open the ``polls/urls.py`` URLconf and change it like so:
.. snippet::
:filename: polls/urls.py
from django.conf.urls import patterns, url
@ -223,7 +237,10 @@ Amend views
Next, we're going to remove our old ``index``, ``detail``, and ``results``
views and use Django's generic views instead. To do so, open the
``polls/views.py`` file and change it like so::
``polls/views.py`` file and change it like so:
.. snippet::
:filename: polls/views.py
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect

View File

@ -160,7 +160,10 @@ A conventional place for an application's tests is in the application's
``tests.py`` file; the testing system will automatically find tests in any file
whose name begins with ``test``.
Put the following in the ``tests.py`` file in the ``polls`` application::
Put the following in the ``tests.py`` file in the ``polls`` application:
.. snippet::
:filename: polls/tests.py
import datetime
@ -236,7 +239,10 @@ Fixing the bug
We already know what the problem is: ``Question.was_published_recently()`` should
return ``False`` if its ``pub_date`` is in the future. Amend the method in
``models.py``, so that it will only return ``True`` if the date is also in the
past::
past:
.. snippet::
:filename: polls/models.py
def was_published_recently(self):
now = timezone.now()
@ -268,7 +274,10 @@ method; in fact, it would be positively embarrassing if in fixing one bug we had
introduced another.
Add two more test methods to the same class, to test the behavior of the method
more comprehensively::
more comprehensively:
.. snippet::
:filename: polls/tests.py
def test_was_published_recently_with_old_question(self):
"""
@ -382,7 +391,10 @@ The list of polls shows polls that aren't published yet (i.e. those that have a
``pub_date`` in the future). Let's fix that.
In :doc:`Tutorial 4 </intro/tutorial04>` we introduced a class-based view,
based on :class:`~django.views.generic.list.ListView`::
based on :class:`~django.views.generic.list.ListView`:
.. snippet::
:filename: polls/views.py
class IndexView(generic.ListView):
template_name = 'polls/index.html'
@ -397,11 +409,17 @@ places into the context.
We need to amend the ``get_queryset`` method and change it so that it also
checks the date by comparing it with ``timezone.now()``. First we need to add
an import::
an import:
.. snippet::
:filename: polls/views.py
from django.utils import timezone
and then we must amend the ``get_queryset`` method like so::
and then we must amend the ``get_queryset`` method like so:
.. snippet::
:filename: polls/views.py
def get_queryset(self):
"""
@ -426,12 +444,18 @@ are listed. You don't want to have to do that *every single time you make any
change that might affect this* - so let's also create a test, based on our
:djadmin:`shell` session above.
Add the following to ``polls/tests.py``::
Add the following to ``polls/tests.py``:
.. snippet::
:filename: polls/tests.py
from django.core.urlresolvers import reverse
and we'll create a factory method to create questions as well as a new test
class::
class:
.. snippet::
:filename: polls/tests.py
def create_question(question_text, days):
"""
@ -532,8 +556,10 @@ Testing the ``DetailView``
What we have works well; however, even though future questions don't appear in
the *index*, users can still reach them if they know or guess the right URL. So
we need to add a similar constraint to ``DetailView``::
we need to add a similar constraint to ``DetailView``:
.. snippet::
:filename: polls/views.py
class DetailView(generic.DetailView):
...
@ -545,7 +571,10 @@ we need to add a similar constraint to ``DetailView``::
And of course, we will add some tests, to check that a ``Question`` whose
``pub_date`` is in the past can be displayed, and that one with a ``pub_date``
in the future is not::
in the future is not:
.. snippet::
:filename: polls/tests.py
class QuestionIndexDetailTests(TestCase):
def test_detail_view_with_a_future_question(self):

View File

@ -56,7 +56,8 @@ reference the path for templates.
Put the following code in that stylesheet (``polls/static/polls/style.css``):
.. code-block:: css
.. snippet:: css
:filename: polls/static/polls/style.css
li a {
color: green;
@ -64,7 +65,8 @@ Put the following code in that stylesheet (``polls/static/polls/style.css``):
Next, add the following at the top of ``polls/templates/polls/index.html``:
.. code-block:: html+django
.. snippet:: html+django
:filename: polls/templates/polls/index.html
{% load staticfiles %}
@ -88,7 +90,8 @@ called ``background.gif``. In other words, put your image in
Then, add to your stylesheet (``polls/static/polls/style.css``):
.. code-block:: css
.. snippet:: css
:filename: polls/static/polls/style.css
body {
background: white url("images/background.gif") no-repeat right bottom;