Fixed #20876 -- Changed Poll model name in tutorial to Question
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 71 KiB |
|
@ -331,22 +331,24 @@ The first step in writing a database Web app in Django is to define your models
|
|||
the :ref:`DRY Principle <dry>`. The goal is to define your data model in one
|
||||
place and automatically derive things from it.
|
||||
|
||||
In our simple poll app, we'll create two models: ``Poll`` and ``Choice``.
|
||||
A ``Poll`` has a question and a publication date. A ``Choice`` has two fields:
|
||||
In our simple poll app, we'll create two models: ``Question`` and ``Choice``.
|
||||
A ``Question`` has a question and a publication date. A ``Choice`` has two fields:
|
||||
the text of the choice and a vote tally. Each ``Choice`` is associated with a
|
||||
``Poll``.
|
||||
``Question``.
|
||||
|
||||
These concepts are represented by simple Python classes. Edit the
|
||||
:file:`polls/models.py` file so it looks like this::
|
||||
|
||||
from django.db import models
|
||||
|
||||
class Poll(models.Model):
|
||||
question = models.CharField(max_length=200)
|
||||
|
||||
class Question(models.Model):
|
||||
question_text = models.CharField(max_length=200)
|
||||
pub_date = models.DateTimeField('date published')
|
||||
|
||||
|
||||
class Choice(models.Model):
|
||||
poll = models.ForeignKey(Poll)
|
||||
question = models.ForeignKey(Question)
|
||||
choice_text = models.CharField(max_length=200)
|
||||
votes = models.IntegerField(default=0)
|
||||
|
||||
|
@ -359,7 +361,7 @@ class -- e.g., :class:`~django.db.models.CharField` for character fields and
|
|||
:class:`~django.db.models.DateTimeField` for datetimes. This tells Django what
|
||||
type of data each field holds.
|
||||
|
||||
The name of each :class:`~django.db.models.Field` instance (e.g. ``question`` or
|
||||
The name of each :class:`~django.db.models.Field` instance (e.g. ``question_text`` or
|
||||
``pub_date``) is the field's name, in machine-friendly format. You'll use this
|
||||
value in your Python code, and your database will use it as the column name.
|
||||
|
||||
|
@ -367,7 +369,7 @@ You can use an optional first positional argument to a
|
|||
:class:`~django.db.models.Field` to designate a human-readable name. That's used
|
||||
in a couple of introspective parts of Django, and it doubles as documentation.
|
||||
If this field isn't provided, Django will use the machine-readable name. In this
|
||||
example, we've only defined a human-readable name for ``Poll.pub_date``. For all
|
||||
example, we've only defined a human-readable name for ``Question.pub_date``. For all
|
||||
other fields in this model, the field's machine-readable name will suffice as
|
||||
its human-readable name.
|
||||
|
||||
|
@ -382,7 +384,7 @@ this case, we've set the :attr:`~django.db.models.Field.default` value of
|
|||
|
||||
Finally, note a relationship is defined, using
|
||||
:class:`~django.db.models.ForeignKey`. That tells Django each ``Choice`` is related
|
||||
to a single ``Poll``. Django supports all the common database relationships:
|
||||
to a single ``Question``. Django supports all the common database relationships:
|
||||
many-to-ones, many-to-manys and one-to-ones.
|
||||
|
||||
.. _`Python path`: http://docs.python.org/tutorial/modules.html#the-module-search-path
|
||||
|
@ -394,7 +396,7 @@ That small bit of model code gives Django a lot of information. With it, Django
|
|||
is able to:
|
||||
|
||||
* Create a database schema (``CREATE TABLE`` statements) for this app.
|
||||
* Create a Python database-access API for accessing ``Poll`` and ``Choice`` objects.
|
||||
* Create a Python database-access API for accessing ``Question`` and ``Choice`` objects.
|
||||
|
||||
But first we need to tell our project that the ``polls`` app is installed.
|
||||
|
||||
|
@ -430,14 +432,14 @@ statements for the polls app):
|
|||
.. code-block:: sql
|
||||
|
||||
BEGIN;
|
||||
CREATE TABLE "polls_poll" (
|
||||
CREATE TABLE "polls_question" (
|
||||
"id" integer NOT NULL PRIMARY KEY,
|
||||
"question" varchar(200) NOT NULL,
|
||||
"question_text" varchar(200) NOT NULL,
|
||||
"pub_date" datetime NOT NULL
|
||||
);
|
||||
CREATE TABLE "polls_choice" (
|
||||
"id" integer NOT NULL PRIMARY KEY,
|
||||
"poll_id" integer NOT NULL REFERENCES "polls_poll" ("id"),
|
||||
"question_id" integer NOT NULL REFERENCES "polls_poll" ("id"),
|
||||
"choice_text" varchar(200) NOT NULL,
|
||||
"votes" integer NOT NULL
|
||||
);
|
||||
|
@ -449,7 +451,7 @@ Note the following:
|
|||
example above is generated for SQLite.
|
||||
|
||||
* Table names are automatically generated by combining the name of the app
|
||||
(``polls``) and the lowercase name of the model -- ``poll`` and
|
||||
(``polls``) and the lowercase name of the model -- ``question`` and
|
||||
``choice``. (You can override this behavior.)
|
||||
|
||||
* Primary keys (IDs) are added automatically. (You can override this, too.)
|
||||
|
@ -537,57 +539,57 @@ the Python import path to your :file:`mysite/settings.py` file.
|
|||
|
||||
Once you're in the shell, explore the :doc:`database API </topics/db/queries>`::
|
||||
|
||||
>>> from polls.models import Poll, Choice # Import the model classes we just wrote.
|
||||
>>> from polls.models import Question, Choice # Import the model classes we just wrote.
|
||||
|
||||
# No polls are in the system yet.
|
||||
>>> Poll.objects.all()
|
||||
# No questions are in the system yet.
|
||||
>>> Question.objects.all()
|
||||
[]
|
||||
|
||||
# Create a new Poll.
|
||||
# Create a new Question.
|
||||
# Support for time zones is enabled in the default settings file, so
|
||||
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
|
||||
# instead of datetime.datetime.now() and it will do the right thing.
|
||||
>>> from django.utils import timezone
|
||||
>>> p = Poll(question="What's new?", pub_date=timezone.now())
|
||||
>>> q = Question(question_text="What's new?", pub_date=timezone.now())
|
||||
|
||||
# Save the object into the database. You have to call save() explicitly.
|
||||
>>> p.save()
|
||||
>>> q.save()
|
||||
|
||||
# Now it has an ID. Note that this might say "1L" instead of "1", depending
|
||||
# on which database you're using. That's no biggie; it just means your
|
||||
# database backend prefers to return integers as Python long integer
|
||||
# objects.
|
||||
>>> p.id
|
||||
>>> q.id
|
||||
1
|
||||
|
||||
# Access database columns via Python attributes.
|
||||
>>> p.question
|
||||
>>> q.question_text
|
||||
"What's new?"
|
||||
>>> p.pub_date
|
||||
>>> q.pub_date
|
||||
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)
|
||||
|
||||
# Change values by changing the attributes, then calling save().
|
||||
>>> p.question = "What's up?"
|
||||
>>> p.save()
|
||||
>>> q.question_text = "What's up?"
|
||||
>>> q.save()
|
||||
|
||||
# objects.all() displays all the polls in the database.
|
||||
>>> Poll.objects.all()
|
||||
[<Poll: Poll object>]
|
||||
# objects.all() displays all the questions in the database.
|
||||
>>> Question.objects.all()
|
||||
[<Question: Question object>]
|
||||
|
||||
|
||||
Wait a minute. ``<Poll: Poll object>`` is, utterly, an unhelpful representation
|
||||
of this object. Let's fix that by editing the polls model (in the
|
||||
Wait a minute. ``<Question: Question object>`` is, utterly, an unhelpful representation
|
||||
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 ``Poll`` and
|
||||
:meth:`~django.db.models.Model.__unicode__` method to both ``Question`` and
|
||||
``Choice``. On Python 3, simply replace ``__unicode__`` by ``__str__`` in the
|
||||
following example::
|
||||
|
||||
from django.db import models
|
||||
|
||||
class Poll(models.Model):
|
||||
class Question(models.Model):
|
||||
# ...
|
||||
def __unicode__(self): # Python 3: def __str__(self):
|
||||
return self.question
|
||||
return self.question_text
|
||||
|
||||
class Choice(models.Model):
|
||||
# ...
|
||||
|
@ -629,7 +631,7 @@ demonstration::
|
|||
import datetime
|
||||
from django.utils import timezone
|
||||
# ...
|
||||
class Poll(models.Model):
|
||||
class Question(models.Model):
|
||||
# ...
|
||||
def was_published_recently(self):
|
||||
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
|
||||
|
@ -643,80 +645,80 @@ the :doc:`time zone support docs </topics/i18n/timezones>`.
|
|||
Save these changes and start a new Python interactive shell by running
|
||||
``python manage.py shell`` again::
|
||||
|
||||
>>> from polls.models import Poll, Choice
|
||||
>>> from polls.models import Question, Choice
|
||||
|
||||
# Make sure our __unicode__() addition worked.
|
||||
>>> Poll.objects.all()
|
||||
[<Poll: What's up?>]
|
||||
>>> Question.objects.all()
|
||||
[<Question: What's up?>]
|
||||
|
||||
# Django provides a rich database lookup API that's entirely driven by
|
||||
# keyword arguments.
|
||||
>>> Poll.objects.filter(id=1)
|
||||
[<Poll: What's up?>]
|
||||
>>> Poll.objects.filter(question__startswith='What')
|
||||
[<Poll: What's up?>]
|
||||
>>> Question.objects.filter(id=1)
|
||||
[<Question: What's up?>]
|
||||
>>> Question.objects.filter(question_text__startswith='What')
|
||||
[<Question: What's up?>]
|
||||
|
||||
# Get the poll that was published this year.
|
||||
# Get the question that was published this year.
|
||||
>>> from django.utils import timezone
|
||||
>>> current_year = timezone.now().year
|
||||
>>> Poll.objects.get(pub_date__year=current_year)
|
||||
<Poll: What's up?>
|
||||
>>> Question.objects.get(pub_date__year=current_year)
|
||||
<Question: What's up?>
|
||||
|
||||
# Request an ID that doesn't exist, this will raise an exception.
|
||||
>>> Poll.objects.get(id=2)
|
||||
>>> Question.objects.get(id=2)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
DoesNotExist: Poll matching query does not exist. Lookup parameters were {'id': 2}
|
||||
DoesNotExist: Question matching query does not exist. Lookup parameters were {'id': 2}
|
||||
|
||||
# Lookup by a primary key is the most common case, so Django provides a
|
||||
# shortcut for primary-key exact lookups.
|
||||
# The following is identical to Poll.objects.get(id=1).
|
||||
>>> Poll.objects.get(pk=1)
|
||||
<Poll: What's up?>
|
||||
# The following is identical to Question.objects.get(id=1).
|
||||
>>> Question.objects.get(pk=1)
|
||||
<Question: What's up?>
|
||||
|
||||
# Make sure our custom method worked.
|
||||
>>> p = Poll.objects.get(pk=1)
|
||||
>>> p.was_published_recently()
|
||||
>>> q = Question.objects.get(pk=1)
|
||||
>>> q.was_published_recently()
|
||||
True
|
||||
|
||||
# Give the Poll a couple of Choices. The create call constructs a new
|
||||
# Give the Question a couple of Choices. The create call constructs a new
|
||||
# Choice object, does the INSERT statement, adds the choice to the set
|
||||
# of available choices and returns the new Choice object. Django creates
|
||||
# a set to hold the "other side" of a ForeignKey relation
|
||||
# (e.g. a poll's choices) which can be accessed via the API.
|
||||
>>> p = Poll.objects.get(pk=1)
|
||||
# (e.g. a question's choice) which can be accessed via the API.
|
||||
>>> q = Question.objects.get(pk=1)
|
||||
|
||||
# Display any choices from the related object set -- none so far.
|
||||
>>> p.choice_set.all()
|
||||
>>> q.choice_set.all()
|
||||
[]
|
||||
|
||||
# Create three choices.
|
||||
>>> p.choice_set.create(choice_text='Not much', votes=0)
|
||||
>>> q.choice_set.create(choice_text='Not much', votes=0)
|
||||
<Choice: Not much>
|
||||
>>> p.choice_set.create(choice_text='The sky', votes=0)
|
||||
>>> q.choice_set.create(choice_text='The sky', votes=0)
|
||||
<Choice: The sky>
|
||||
>>> c = p.choice_set.create(choice_text='Just hacking again', votes=0)
|
||||
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)
|
||||
|
||||
# Choice objects have API access to their related Poll objects.
|
||||
>>> c.poll
|
||||
<Poll: What's up?>
|
||||
# Choice objects have API access to their related Question objects.
|
||||
>>> c.question
|
||||
<Question: What's up?>
|
||||
|
||||
# And vice versa: Poll objects get access to Choice objects.
|
||||
>>> p.choice_set.all()
|
||||
# And vice versa: Question objects get access to Choice objects.
|
||||
>>> q.choice_set.all()
|
||||
[<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]
|
||||
>>> p.choice_set.count()
|
||||
>>> q.choice_set.count()
|
||||
3
|
||||
|
||||
# The API automatically follows relationships as far as you need.
|
||||
# Use double underscores to separate relationships.
|
||||
# This works as many levels deep as you want; there's no limit.
|
||||
# Find all Choices for any poll whose pub_date is in this year
|
||||
# Find all Choices for any question whose pub_date is in this year
|
||||
# (reusing the 'current_year' variable we created above).
|
||||
>>> Choice.objects.filter(poll__pub_date__year=current_year)
|
||||
>>> Choice.objects.filter(question__pub_date__year=current_year)
|
||||
[<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]
|
||||
|
||||
# Let's delete one of the choices. Use delete() for that.
|
||||
>>> c = p.choice_set.filter(choice_text__startswith='Just hacking')
|
||||
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
|
||||
>>> c.delete()
|
||||
|
||||
For more information on model relations, see :doc:`Accessing related objects
|
||||
|
|
|
@ -65,7 +65,7 @@ tutorial, remember? If you didn't create one or forgot the password you can
|
|||
|
||||
You should see the Django admin index page:
|
||||
|
||||
.. image:: _images/admin02t.png
|
||||
.. image:: _images/admin02.png
|
||||
:alt: Django admin index page
|
||||
|
||||
You should see a few types of editable content: groups and users. They are
|
||||
|
@ -77,39 +77,39 @@ Make the poll app modifiable in the admin
|
|||
|
||||
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 ``Poll``
|
||||
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::
|
||||
|
||||
from django.contrib import admin
|
||||
from polls.models import Poll
|
||||
from polls.models import Question
|
||||
|
||||
admin.site.register(Poll)
|
||||
admin.site.register(Question)
|
||||
|
||||
Explore the free admin functionality
|
||||
====================================
|
||||
|
||||
Now that we've registered ``Poll``, Django knows that it should be displayed on
|
||||
Now that we've registered ``Question``, Django knows that it should be displayed on
|
||||
the admin index page:
|
||||
|
||||
.. image:: _images/admin03t.png
|
||||
:alt: Django admin index page, now with polls displayed
|
||||
|
||||
Click "Polls." Now you're at the "change list" page for polls. This page
|
||||
displays all the polls in the database and lets you choose one to change it.
|
||||
There's the "What's up?" poll we created in the first tutorial:
|
||||
Click "Questions". Now you're at the "change list" page for questions. This page
|
||||
displays all the question in the database and lets you choose one to change it.
|
||||
There's the "What's up?" question we created in the first tutorial:
|
||||
|
||||
.. image:: _images/admin04t.png
|
||||
:alt: Polls change list page
|
||||
|
||||
Click the "What's up?" poll to edit it:
|
||||
Click the "What's up?" question to edit it:
|
||||
|
||||
.. image:: _images/admin05t.png
|
||||
:alt: Editing form for poll object
|
||||
:alt: Editing form for question object
|
||||
|
||||
Things to note here:
|
||||
|
||||
* The form is automatically generated from the ``Poll`` model.
|
||||
* The form is automatically generated from the ``Question`` model.
|
||||
|
||||
* The different model field types (:class:`~django.db.models.DateTimeField`,
|
||||
:class:`~django.db.models.CharField`) correspond to the appropriate HTML
|
||||
|
@ -134,7 +134,7 @@ The bottom part of the page gives you a couple of options:
|
|||
* Delete -- Displays a delete confirmation page.
|
||||
|
||||
If the value of "Date published" doesn't match the time when you created the
|
||||
poll in Tutorial 1, it probably means you forgot to set the correct value for
|
||||
question in Tutorial 1, it probably means you forgot to set the correct value for
|
||||
the :setting:`TIME_ZONE` setting. Change it, reload the page and check that
|
||||
the correct value appears.
|
||||
|
||||
|
@ -144,27 +144,28 @@ You'll see a page listing all changes made to this object via the Django admin,
|
|||
with the timestamp and username of the person who made the change:
|
||||
|
||||
.. image:: _images/admin06t.png
|
||||
:alt: History page for poll object
|
||||
:alt: History page for question object
|
||||
|
||||
Customize the admin form
|
||||
========================
|
||||
|
||||
Take a few minutes to marvel at all the code you didn't have to write. By
|
||||
registering the Poll model with ``admin.site.register(Poll)``, Django was able
|
||||
to construct a default form representation. Often, you'll want 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.
|
||||
registering the ``Question`` model with ``admin.site.register(Question)``,
|
||||
Django was able to construct a default form representation. Often, you'll want
|
||||
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(Poll)`` line with::
|
||||
the ``admin.site.register(Question)`` line with::
|
||||
|
||||
from django.contrib import admin
|
||||
from polls.models import Poll
|
||||
from polls.models import Question
|
||||
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
fields = ['pub_date', 'question']
|
||||
|
||||
admin.site.register(Poll, PollAdmin)
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
fields = ['pub_date', 'question_text']
|
||||
|
||||
admin.site.register(Question, QuestionAdmin)
|
||||
|
||||
You'll follow this pattern -- create a model admin object, then pass it as the
|
||||
second argument to ``admin.site.register()`` -- any time you need to change the
|
||||
|
@ -183,15 +184,16 @@ And speaking of forms with dozens of fields, you might want to split the form
|
|||
up into fieldsets::
|
||||
|
||||
from django.contrib import admin
|
||||
from polls.models import Poll
|
||||
from polls.models import Question
|
||||
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
(None, {'fields': ['question']}),
|
||||
(None, {'fields': ['question_text']}),
|
||||
('Date information', {'fields': ['pub_date']}),
|
||||
]
|
||||
|
||||
admin.site.register(Poll, PollAdmin)
|
||||
admin.site.register(Question, QuestionAdmin)
|
||||
|
||||
The first element of each tuple in ``fieldsets`` is the title of the fieldset.
|
||||
Here's what our form looks like now:
|
||||
|
@ -205,11 +207,12 @@ This is useful when you have a long form that contains a number of fields that
|
|||
aren't commonly used::
|
||||
|
||||
from django.contrib import admin
|
||||
from polls.models import Poll
|
||||
from polls.models import Question
|
||||
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
(None, {'fields': ['question']}),
|
||||
(None, {'fields': ['question_text']}),
|
||||
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
|
||||
]
|
||||
|
||||
|
@ -219,13 +222,13 @@ aren't commonly used::
|
|||
Adding related objects
|
||||
======================
|
||||
|
||||
OK, we have our Poll admin page. But a ``Poll`` has multiple ``Choices``, and
|
||||
OK, we have our Question admin page. But a ``Question`` has multiple ``Choices``, and
|
||||
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 ``Poll``. That's easy::
|
||||
with the admin just as we did with ``Question``. That's easy::
|
||||
|
||||
from django.contrib import admin
|
||||
from polls.models import Choice
|
||||
|
@ -238,48 +241,51 @@ looks like this:
|
|||
.. image:: _images/admin10.png
|
||||
:alt: Choice admin page
|
||||
|
||||
In that form, the "Poll" field is a select box containing every poll in the
|
||||
In that form, the "Question" field is a select box containing every question in the
|
||||
database. Django knows that a :class:`~django.db.models.ForeignKey` should be
|
||||
represented in the admin as a ``<select>`` box. In our case, only one poll
|
||||
represented in the admin as a ``<select>`` box. In our case, only one question
|
||||
exists at this point.
|
||||
|
||||
Also note the "Add Another" link next to "Poll." Every object with a
|
||||
Also note the "Add Another" link next to "Question." Every object with a
|
||||
``ForeignKey`` relationship to another gets this for free. When you click "Add
|
||||
Another," you'll get a popup window with the "Add poll" form. If you add a poll
|
||||
in that window and click "Save," Django will save the poll to the database and
|
||||
Another," you'll get a popup window with the "Add question" form. If you add a question
|
||||
in that window and click "Save," Django will save the question to the database and
|
||||
dynamically add it as the selected choice on the "Add choice" form you're
|
||||
looking at.
|
||||
|
||||
But, really, this is an inefficient way of adding ``Choice`` objects to the system.
|
||||
It'd be better if you could add a bunch of Choices directly when you create the
|
||||
``Poll`` object. Let's make that happen.
|
||||
``Question`` object. Let's make that happen.
|
||||
|
||||
Remove the ``register()`` call for the ``Choice`` model. Then, edit the ``Poll``
|
||||
Remove the ``register()`` call for the ``Choice`` model. Then, edit the ``Question``
|
||||
registration code to read::
|
||||
|
||||
from django.contrib import admin
|
||||
from polls.models import Choice, Poll
|
||||
from polls.models import Choice, Question
|
||||
|
||||
|
||||
class ChoiceInline(admin.StackedInline):
|
||||
model = Choice
|
||||
extra = 3
|
||||
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
(None, {'fields': ['question']}),
|
||||
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
|
||||
(None, {'fields': ['question_text']}),
|
||||
('Date information', {'fields': ['pub_date'],
|
||||
'classes': ['collapse']}),
|
||||
]
|
||||
inlines = [ChoiceInline]
|
||||
|
||||
admin.site.register(Poll, PollAdmin)
|
||||
admin.site.register(Question, QuestionAdmin)
|
||||
|
||||
This tells Django: "``Choice`` objects are edited on the ``Poll`` admin page. By
|
||||
This tells Django: "``Choice`` objects are edited on the ``Question`` admin page. By
|
||||
default, provide enough fields for 3 choices."
|
||||
|
||||
Load the "Add poll" page to see how that looks:
|
||||
Load the "Add question" page to see how that looks:
|
||||
|
||||
.. image:: _images/admin11t.png
|
||||
:alt: Add poll page now has choices on it
|
||||
:alt: Add question page now has choices on it
|
||||
|
||||
It works like this: There are three slots for related Choices -- as specified
|
||||
by ``extra`` -- and each time you come back to the "Change" page for an
|
||||
|
@ -305,7 +311,7 @@ With that ``TabularInline`` (instead of ``StackedInline``), the
|
|||
related objects are displayed in a more compact, table-based format:
|
||||
|
||||
.. image:: _images/admin12t.png
|
||||
:alt: Add poll page now has more compact choices
|
||||
:alt: Add question page now has more compact choices
|
||||
|
||||
Note that there is an extra "Delete?" column that allows removing rows added
|
||||
using the "Add Another Choice" button and rows that have already been saved.
|
||||
|
@ -313,8 +319,8 @@ using the "Add Another Choice" button and rows that have already been saved.
|
|||
Customize the admin change list
|
||||
===============================
|
||||
|
||||
Now that the Poll admin page is looking good, let's make some tweaks to the
|
||||
"change list" page -- the one that displays all the polls in the system.
|
||||
Now that the Question admin page is looking good, let's make some tweaks to the
|
||||
"change list" page -- the one that displays all the questions in the system.
|
||||
|
||||
Here's what it looks like at this point:
|
||||
|
||||
|
@ -326,18 +332,18 @@ 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::
|
||||
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
# ...
|
||||
list_display = ('question', 'pub_date')
|
||||
list_display = ('question_text', 'pub_date')
|
||||
|
||||
Just for good measure, let's also include the ``was_published_recently`` custom
|
||||
method from Tutorial 1::
|
||||
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
# ...
|
||||
list_display = ('question', 'pub_date', 'was_published_recently')
|
||||
list_display = ('question_text', 'pub_date', 'was_published_recently')
|
||||
|
||||
Now the poll change list page looks like this:
|
||||
Now the question change list page looks like this:
|
||||
|
||||
.. image:: _images/admin13t.png
|
||||
:alt: Polls change list page, updated
|
||||
|
@ -352,7 +358,7 @@ representation of the output.
|
|||
You can improve that by giving that method (in :file:`polls/models.py`) a few
|
||||
attributes, as follows::
|
||||
|
||||
class Poll(models.Model):
|
||||
class Question(models.Model):
|
||||
# ...
|
||||
def was_published_recently(self):
|
||||
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
|
||||
|
@ -360,8 +366,8 @@ attributes, as follows::
|
|||
was_published_recently.boolean = True
|
||||
was_published_recently.short_description = 'Published recently?'
|
||||
|
||||
Edit your :file:`polls/admin.py` file again and add an improvement to the Poll
|
||||
change list page: Filters. Add the following line to ``PollAdmin``::
|
||||
Edit your :file:`polls/admin.py` file again and add an improvement to the Question
|
||||
change list page: Filters. Add the following line to ``QuestionAdmin``::
|
||||
|
||||
list_filter = ['pub_date']
|
||||
|
||||
|
@ -378,10 +384,10 @@ knows to give appropriate filter options: "Any date," "Today," "Past 7 days,"
|
|||
|
||||
This is shaping up well. Let's add some search capability::
|
||||
|
||||
search_fields = ['question']
|
||||
search_fields = ['question_text']
|
||||
|
||||
That adds a search box at the top of the change list. When somebody enters
|
||||
search terms, Django will search the ``question`` field. You can use as many
|
||||
search terms, Django will search the ``question_text`` field. You can use as many
|
||||
fields as you'd like -- although because it uses a ``LIKE`` query behind the
|
||||
scenes, keep it reasonable, to keep your database happy.
|
||||
|
||||
|
|
|
@ -29,15 +29,15 @@ application, you might have the following views:
|
|||
|
||||
In our poll application, we'll have the following four views:
|
||||
|
||||
* Poll "index" page -- displays the latest few polls.
|
||||
* Question "index" page -- displays the latest few questions.
|
||||
|
||||
* Poll "detail" page -- displays a poll question, with no results but
|
||||
* Question "detail" page -- displays a question text, with no results but
|
||||
with a form to vote.
|
||||
|
||||
* Poll "results" page -- displays results for a particular poll.
|
||||
* Question "results" page -- displays results for a particular question.
|
||||
|
||||
* Vote action -- handles voting for a particular choice in a particular
|
||||
poll.
|
||||
question.
|
||||
|
||||
In Django, web pages and other content are delivered by views. Each view is
|
||||
represented by a simple Python function (or method, in the case of class-based
|
||||
|
@ -66,8 +66,9 @@ and put the following Python code in it::
|
|||
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
def index(request):
|
||||
return HttpResponse("Hello, world. You're at the poll index.")
|
||||
return HttpResponse("Hello, world. You're at the polls index.")
|
||||
|
||||
This is the simplest view possible in Django. To call the view, we need to map
|
||||
it to a URL - and for this we need a URLconf.
|
||||
|
@ -109,7 +110,7 @@ with::
|
|||
|
||||
You have now wired an ``index`` view into the URLconf. Go to
|
||||
http://localhost:8000/polls/ in your browser, and you should see the text
|
||||
"*Hello, world. You're at the poll index.*", which you defined in the
|
||||
"*Hello, world. You're at the polls index.*", which you defined in the
|
||||
``index`` view.
|
||||
|
||||
The :func:`~django.conf.urls.url` function is passed four arguments, two
|
||||
|
@ -173,14 +174,15 @@ 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::
|
||||
|
||||
def detail(request, poll_id):
|
||||
return HttpResponse("You're looking at poll %s." % poll_id)
|
||||
def detail(request, question_id):
|
||||
return HttpResponse("You're looking at question %s." % question_id)
|
||||
|
||||
def results(request, poll_id):
|
||||
return HttpResponse("You're looking at the results of poll %s." % poll_id)
|
||||
def results(request, question_id):
|
||||
response = "You're looking at the results of question %s."
|
||||
return HttpResponse(response % question_id)
|
||||
|
||||
def vote(request, poll_id):
|
||||
return HttpResponse("You're voting on poll %s." % poll_id)
|
||||
def vote(request, question_id):
|
||||
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::
|
||||
|
@ -193,11 +195,11 @@ Wire these new views into the ``polls.urls`` module by adding the following
|
|||
# ex: /polls/
|
||||
url(r'^$', views.index, name='index'),
|
||||
# ex: /polls/5/
|
||||
url(r'^(?P<poll_id>\d+)/$', views.detail, name='detail'),
|
||||
url(r'^(?P<question_id>\d+)/$', views.detail, name='detail'),
|
||||
# ex: /polls/5/results/
|
||||
url(r'^(?P<poll_id>\d+)/results/$', views.results, name='results'),
|
||||
url(r'^(?P<question_id>\d+)/results/$', views.results, name='results'),
|
||||
# ex: /polls/5/vote/
|
||||
url(r'^(?P<poll_id>\d+)/vote/$', views.vote, name='vote'),
|
||||
url(r'^(?P<question_id>\d+)/vote/$', views.vote, name='vote'),
|
||||
)
|
||||
|
||||
Take a look in your browser, at "/polls/34/". It'll run the ``detail()``
|
||||
|
@ -229,14 +231,14 @@ Here's what happens if a user goes to "/polls/34/" in this system:
|
|||
|
||||
* Then, Django will strip off the matching text (``"polls/"``) and send the
|
||||
remaining text -- ``"34/"`` -- to the 'polls.urls' URLconf for
|
||||
further processing which matches ``r'^(?P<poll_id>\d+)/$'`` resulting in a
|
||||
further processing which matches ``r'^(?P<question_id>\d+)/$'`` resulting in a
|
||||
call to the ``detail()`` view like so::
|
||||
|
||||
detail(request=<HttpRequest object>, poll_id='34')
|
||||
detail(request=<HttpRequest object>, question_id='34')
|
||||
|
||||
The ``poll_id='34'`` part comes from ``(?P<poll_id>\d+)``. Using parentheses
|
||||
The ``question_id='34'`` part comes from ``(?P<question_id>\d+)``. Using parentheses
|
||||
around a pattern "captures" the text matched by that pattern and sends it as an
|
||||
argument to the view function; ``?P<poll_id>`` defines the name that will
|
||||
argument to the view function; ``?P<question_id>`` defines the name that will
|
||||
be used to identify the matched pattern; and ``\d+`` is a regular expression to
|
||||
match a sequence of digits (i.e., a number).
|
||||
|
||||
|
@ -271,11 +273,12 @@ commas, according to publication date::
|
|||
|
||||
from django.http import HttpResponse
|
||||
|
||||
from polls.models import Poll
|
||||
from polls.models import Question
|
||||
|
||||
|
||||
def index(request):
|
||||
latest_poll_list = Poll.objects.order_by('-pub_date')[:5]
|
||||
output = ', '.join([p.question for p in latest_poll_list])
|
||||
latest_question_list = Question.objects.order_by('-pub_date')[:5]
|
||||
output = ', '.join([p.question_text for p in latest_question_list])
|
||||
return HttpResponse(output)
|
||||
|
||||
There's a problem here, though: the page's design is hard-coded in the view. If
|
||||
|
@ -326,10 +329,10 @@ Put the following code in that template:
|
|||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if latest_poll_list %}
|
||||
{% if latest_question_list %}
|
||||
<ul>
|
||||
{% for poll in latest_poll_list %}
|
||||
<li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
|
||||
{% for question in latest_question_list %}
|
||||
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
|
@ -341,13 +344,14 @@ Now let's update our ``index`` view in ``polls/views.py`` to use the template::
|
|||
from django.http import HttpResponse
|
||||
from django.template import RequestContext, loader
|
||||
|
||||
from polls.models import Poll
|
||||
from polls.models import Question
|
||||
|
||||
|
||||
def index(request):
|
||||
latest_poll_list = Poll.objects.order_by('-pub_date')[:5]
|
||||
latest_question_list = Question.objects.order_by('-pub_date')[:5]
|
||||
template = loader.get_template('polls/index.html')
|
||||
context = RequestContext(request, {
|
||||
'latest_poll_list': latest_poll_list,
|
||||
'latest_question_list': latest_question_list,
|
||||
})
|
||||
return HttpResponse(template.render(context))
|
||||
|
||||
|
@ -356,8 +360,8 @@ context. The context is a dictionary mapping template variable names to Python
|
|||
objects.
|
||||
|
||||
Load the page by pointing your browser at "/polls/", 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.
|
||||
bulleted-list containing the "What's up" question from Tutorial 1. The link points
|
||||
to the question's detail page.
|
||||
|
||||
A shortcut: :func:`~django.shortcuts.render`
|
||||
--------------------------------------------
|
||||
|
@ -369,11 +373,12 @@ rewritten::
|
|||
|
||||
from django.shortcuts import render
|
||||
|
||||
from polls.models import Poll
|
||||
from polls.models import Question
|
||||
|
||||
|
||||
def index(request):
|
||||
latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
|
||||
context = {'latest_poll_list': latest_poll_list}
|
||||
latest_question_list = Question.objects.all().order_by('-pub_date')[:5]
|
||||
context = {'latest_question_list': latest_question_list}
|
||||
return render(request, 'polls/index.html', context)
|
||||
|
||||
Note that once we've done this in all these views, we no longer need to import
|
||||
|
@ -389,29 +394,29 @@ object of the given template rendered with the given context.
|
|||
Raising a 404 error
|
||||
===================
|
||||
|
||||
Now, let's tackle the poll detail view -- the page that displays the question
|
||||
Now, let's tackle the question detail view -- the page that displays the question text
|
||||
for a given poll. Here's the view::
|
||||
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
|
||||
from polls.models import Poll
|
||||
from polls.models import Question
|
||||
# ...
|
||||
def detail(request, poll_id):
|
||||
def detail(request, question_id):
|
||||
try:
|
||||
poll = Poll.objects.get(pk=poll_id)
|
||||
except Poll.DoesNotExist:
|
||||
question = Question.objects.get(pk=question_id)
|
||||
except Question.DoesNotExist:
|
||||
raise Http404
|
||||
return render(request, 'polls/detail.html', {'poll': poll})
|
||||
return render(request, 'polls/detail.html', {'question': question})
|
||||
|
||||
The new concept here: The view raises the :exc:`~django.http.Http404` exception
|
||||
if a poll with the requested ID doesn't exist.
|
||||
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::
|
||||
|
||||
{{ poll }}
|
||||
{{ question }}
|
||||
|
||||
will get you started for now.
|
||||
|
||||
|
@ -424,11 +429,11 @@ provides a shortcut. Here's the ``detail()`` view, rewritten::
|
|||
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
|
||||
from polls.models import Poll
|
||||
from polls.models import Question
|
||||
# ...
|
||||
def detail(request, poll_id):
|
||||
poll = get_object_or_404(Poll, pk=poll_id)
|
||||
return render(request, 'polls/detail.html', {'poll': poll})
|
||||
def detail(request, question_id):
|
||||
question = get_object_or_404(Question, pk=question_id)
|
||||
return render(request, 'polls/detail.html', {'question': question})
|
||||
|
||||
The :func:`~django.shortcuts.get_object_or_404` function takes a Django model
|
||||
as its first argument and an arbitrary number of keyword arguments, which it
|
||||
|
@ -458,27 +463,27 @@ 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
|
||||
variable ``question``, here's what the ``polls/detail.html`` template might look
|
||||
like:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<h1>{{ poll.question }}</h1>
|
||||
<h1>{{ question.question_text }}</h1>
|
||||
<ul>
|
||||
{% for choice in poll.choice_set.all %}
|
||||
{% for choice in question.choice_set.all %}
|
||||
<li>{{ choice.choice_text }}</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
|
||||
the example of ``{{ question.question_text }}``, first Django does a dictionary lookup
|
||||
on the object ``question``. 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 :ttag:`{% for %}<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
|
||||
``question.choice_set.all`` is interpreted as the Python code
|
||||
``question.choice_set.all()``, which returns an iterable of ``Choice`` objects and is
|
||||
suitable for use in the :ttag:`{% for %}<for>` tag.
|
||||
|
||||
See the :doc:`template guide </topics/templates>` for more about templates.
|
||||
|
@ -486,12 +491,12 @@ See the :doc:`template guide </topics/templates>` for more about templates.
|
|||
Removing hardcoded URLs in templates
|
||||
====================================
|
||||
|
||||
Remember, when we wrote the link to a poll in the ``polls/index.html``
|
||||
Remember, when we wrote the link to a question in the ``polls/index.html``
|
||||
template, the link was partially hardcoded like this:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
|
||||
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
|
||||
|
||||
The problem with this hardcoded, tightly-coupled approach is that it becomes
|
||||
challenging to change URLs on projects with a lot of templates. However, since
|
||||
|
@ -501,12 +506,12 @@ defined in your url configurations by using the ``{% url %}`` template tag:
|
|||
|
||||
.. code-block:: html+django
|
||||
|
||||
<li><a href="{% url 'detail' poll.id %}">{{ poll.question }}</a></li>
|
||||
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
|
||||
|
||||
.. note::
|
||||
|
||||
If ``{% url 'detail' poll.id %}`` (with quotes) doesn't work, but
|
||||
``{% url detail poll.id %}`` (without quotes) does, that means you're
|
||||
If ``{% url 'detail' question.id %}`` (with quotes) doesn't work, but
|
||||
``{% url detail question.id %}`` (without quotes) does, that means you're
|
||||
using a version of Django < 1.5. In this case, add the following
|
||||
declaration at the top of your template:
|
||||
|
||||
|
@ -520,7 +525,7 @@ defined below::
|
|||
|
||||
...
|
||||
# the 'name' value as called by the {% url %} template tag
|
||||
url(r'^(?P<poll_id>\d+)/$', views.detail, name='detail'),
|
||||
url(r'^(?P<question_id>\d+)/$', views.detail, name='detail'),
|
||||
...
|
||||
|
||||
If you want to change the URL of the polls detail view to something else,
|
||||
|
@ -529,7 +534,7 @@ template (or templates) you would change it in ``polls/urls.py``::
|
|||
|
||||
...
|
||||
# added the word 'specifics'
|
||||
url(r'^specifics/(?P<poll_id>\d+)/$', views.detail, name='detail'),
|
||||
url(r'^specifics/(?P<question_id>\d+)/$', views.detail, name='detail'),
|
||||
...
|
||||
|
||||
Namespacing URL names
|
||||
|
@ -560,13 +565,13 @@ Now change your ``polls/index.html`` template from:
|
|||
|
||||
.. code-block:: html+django
|
||||
|
||||
<li><a href="{% url 'detail' poll.id %}">{{ poll.question }}</a></li>
|
||||
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
|
||||
|
||||
to point at the namespaced detail view:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<li><a href="{% url 'polls:detail' poll.id %}">{{ poll.question }}</a></li>
|
||||
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
|
||||
|
||||
When you're comfortable with writing views, read :doc:`part 4 of this tutorial
|
||||
</intro/tutorial04>` to learn about simple form processing and generic views.
|
||||
|
|
|
@ -14,13 +14,13 @@ tutorial, so that the template contains an HTML ``<form>`` element:
|
|||
|
||||
.. code-block:: html+django
|
||||
|
||||
<h1>{{ poll.question }}</h1>
|
||||
<h1>{{ question.question_text }}</h1>
|
||||
|
||||
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
|
||||
|
||||
<form action="{% url 'polls:vote' poll.id %}" method="post">
|
||||
<form action="{% url 'polls:vote' question.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
{% for choice in poll.choice_set.all %}
|
||||
{% for choice in question.choice_set.all %}
|
||||
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
|
||||
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
|
||||
{% endfor %}
|
||||
|
@ -29,13 +29,13 @@ tutorial, so that the template contains an HTML ``<form>`` element:
|
|||
|
||||
A quick rundown:
|
||||
|
||||
* The above template displays a radio button for each poll choice. The
|
||||
``value`` of each radio button is the associated poll choice's ID. The
|
||||
* The above template displays a radio button for each question choice. The
|
||||
``value`` of each radio button is the associated question choice's ID. The
|
||||
``name`` of each radio button is ``"choice"``. That means, when somebody
|
||||
selects one of the radio buttons and submits the form, it'll send the
|
||||
POST data ``choice=3``. This is the basic concept of HTML forms.
|
||||
|
||||
* We set the form's ``action`` to ``{% url 'polls:vote' poll.id %}``, and we
|
||||
* We set the form's ``action`` to ``{% url 'polls:vote' question.id %}``, and we
|
||||
set ``method="post"``. Using ``method="post"`` (as opposed to
|
||||
``method="get"``) is very important, because the act of submitting this
|
||||
form will alter data server-side. Whenever you create a form that alters
|
||||
|
@ -56,7 +56,7 @@ 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::
|
||||
|
||||
url(r'^(?P<poll_id>\d+)/vote/$', views.vote, name='vote'),
|
||||
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``::
|
||||
|
@ -64,16 +64,16 @@ create a real version. Add the following to ``polls/views.py``::
|
|||
from django.shortcuts import get_object_or_404, render
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.core.urlresolvers import reverse
|
||||
from polls.models import Choice, Poll
|
||||
from polls.models import Choice, Question
|
||||
# ...
|
||||
def vote(request, poll_id):
|
||||
p = get_object_or_404(Poll, pk=poll_id)
|
||||
def vote(request, question_id):
|
||||
p = get_object_or_404(Question, pk=question_id)
|
||||
try:
|
||||
selected_choice = p.choice_set.get(pk=request.POST['choice'])
|
||||
except (KeyError, Choice.DoesNotExist):
|
||||
# Redisplay the poll voting form.
|
||||
# Redisplay the question voting form.
|
||||
return render(request, 'polls/detail.html', {
|
||||
'poll': p,
|
||||
'question': p,
|
||||
'error_message': "You didn't select a choice.",
|
||||
})
|
||||
else:
|
||||
|
@ -100,7 +100,7 @@ This code includes a few things we haven't covered yet in this tutorial:
|
|||
|
||||
* ``request.POST['choice']`` will raise :exc:`~exceptions.KeyError` if
|
||||
``choice`` wasn't provided in POST data. The above code checks for
|
||||
:exc:`~exceptions.KeyError` and redisplays the poll form with an error
|
||||
:exc:`~exceptions.KeyError` and redisplays the question form with an error
|
||||
message if ``choice`` isn't given.
|
||||
|
||||
* After incrementing the choice count, the code returns an
|
||||
|
@ -133,14 +133,15 @@ As mentioned in Tutorial 3, ``request`` is a :class:`~django.http.HttpRequest`
|
|||
object. For more on :class:`~django.http.HttpRequest` objects, see the
|
||||
:doc:`request and response documentation </ref/request-response>`.
|
||||
|
||||
After somebody votes in a poll, the ``vote()`` view redirects to the results
|
||||
page for the poll. Let's write that view::
|
||||
After somebody votes in a question, the ``vote()`` view redirects to the results
|
||||
page for the question. Let's write that view::
|
||||
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
def results(request, poll_id):
|
||||
poll = get_object_or_404(Poll, pk=poll_id)
|
||||
return render(request, 'polls/results.html', {'poll': poll})
|
||||
|
||||
def results(request, question_id):
|
||||
question = get_object_or_404(Question, pk=question_id)
|
||||
return render(request, 'polls/results.html', {'question': question})
|
||||
|
||||
This is almost exactly the same as the ``detail()`` view from :doc:`Tutorial 3
|
||||
</intro/tutorial03>`. The only difference is the template name. We'll fix this
|
||||
|
@ -150,17 +151,17 @@ Now, create a ``polls/results.html`` template:
|
|||
|
||||
.. code-block:: html+django
|
||||
|
||||
<h1>{{ poll.question }}</h1>
|
||||
<h1>{{ question.question_text }}</h1>
|
||||
|
||||
<ul>
|
||||
{% for choice in poll.choice_set.all %}
|
||||
{% for choice in question.choice_set.all %}
|
||||
<li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<a href="{% url 'polls:detail' poll.id %}">Vote again?</a>
|
||||
<a href="{% url 'polls:detail' question.id %}">Vote again?</a>
|
||||
|
||||
Now, go to ``/polls/1/`` in your browser and vote in the poll. You should see a
|
||||
Now, go to ``/polls/1/`` in your browser and vote in the question. You should see a
|
||||
results page that gets updated each time you vote. If you submit the form
|
||||
without having chosen a choice, you should see the error message.
|
||||
|
||||
|
@ -214,7 +215,7 @@ First, open the ``polls/urls.py`` URLconf and change it like so::
|
|||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
|
||||
url(r'^(?P<pk>\d+)/results/$', views.ResultsView.as_view(), name='results'),
|
||||
url(r'^(?P<poll_id>\d+)/vote/$', views.vote, name='vote'),
|
||||
url(r'^(?P<question_id>\d+)/vote/$', views.vote, name='vote'),
|
||||
)
|
||||
|
||||
Amend views
|
||||
|
@ -229,27 +230,29 @@ views and use Django's generic views instead. To do so, open the
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.views import generic
|
||||
|
||||
from polls.models import Choice, Poll
|
||||
from polls.models import Choice, Question
|
||||
|
||||
|
||||
class IndexView(generic.ListView):
|
||||
template_name = 'polls/index.html'
|
||||
context_object_name = 'latest_poll_list'
|
||||
context_object_name = 'latest_question_list'
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the last five published polls."""
|
||||
return Poll.objects.order_by('-pub_date')[:5]
|
||||
"""Return the last five published questions."""
|
||||
return Question.objects.order_by('-pub_date')[:5]
|
||||
|
||||
|
||||
class DetailView(generic.DetailView):
|
||||
model = Poll
|
||||
model = Question
|
||||
template_name = 'polls/detail.html'
|
||||
|
||||
|
||||
class ResultsView(generic.DetailView):
|
||||
model = Poll
|
||||
model = Question
|
||||
template_name = 'polls/results.html'
|
||||
|
||||
def vote(request, poll_id):
|
||||
|
||||
def vote(request, question_id):
|
||||
....
|
||||
|
||||
We're using two generic views here:
|
||||
|
@ -263,12 +266,12 @@ two views abstract the concepts of "display a list of objects" and
|
|||
|
||||
* The :class:`~django.views.generic.detail.DetailView` generic view
|
||||
expects the primary key value captured from the URL to be called
|
||||
``"pk"``, so we've changed ``poll_id`` to ``pk`` for the generic
|
||||
``"pk"``, so we've changed ``question_id`` to ``pk`` for the generic
|
||||
views.
|
||||
|
||||
By default, the :class:`~django.views.generic.detail.DetailView` generic
|
||||
view uses a template called ``<app name>/<model name>_detail.html``.
|
||||
In our case, it'll use the template ``"polls/poll_detail.html"``. The
|
||||
In our case, it'll use the template ``"polls/question_detail.html"``. The
|
||||
``template_name`` attribute is used to tell Django to use a specific
|
||||
template name instead of the autogenerated default template name. We
|
||||
also specify the ``template_name`` for the ``results`` list view --
|
||||
|
@ -283,13 +286,13 @@ name>_list.html``; we use ``template_name`` to tell
|
|||
``"polls/index.html"`` template.
|
||||
|
||||
In previous parts of the tutorial, the templates have been provided
|
||||
with a context that contains the ``poll`` and ``latest_poll_list``
|
||||
context variables. For ``DetailView`` the ``poll`` variable is provided
|
||||
automatically -- since we're using a Django model (``Poll``), Django
|
||||
with a context that contains the ``question`` and ``latest_question_list``
|
||||
context variables. For ``DetailView`` the ``question`` variable is provided
|
||||
automatically -- since we're using a Django model (``Question``), Django
|
||||
is able to determine an appropriate name for the context variable.
|
||||
However, for ListView, the automatically generated context variable is
|
||||
``poll_list``. To override this we provide the ``context_object_name``
|
||||
attribute, specifying that we want to use ``latest_poll_list`` instead.
|
||||
``question_list``. To override this we provide the ``context_object_name``
|
||||
attribute, specifying that we want to use ``latest_question_list`` instead.
|
||||
As an alternative approach, you could change your templates to match
|
||||
the new default context variables -- but it's a lot easier to just
|
||||
tell Django to use the variable you want.
|
||||
|
|
|
@ -130,22 +130,22 @@ We identify a bug
|
|||
-----------------
|
||||
|
||||
Fortunately, there's a little bug in the ``polls`` application for us to fix
|
||||
right away: the ``Poll.was_published_recently()`` method returns ``True`` if
|
||||
the ``Poll`` was published within the last day (which is correct) but also if
|
||||
the ``Poll``’s ``pub_date`` field is in the future (which certainly isn't).
|
||||
right away: the ``Question.was_published_recently()`` method returns ``True`` if
|
||||
the ``Question`` was published within the last day (which is correct) but also if
|
||||
the ``Question``’s ``pub_date`` field is in the future (which certainly isn't).
|
||||
|
||||
You can see this in the Admin; create a poll whose date lies in the future;
|
||||
you'll see that the ``Poll`` change list claims it was published recently.
|
||||
You can see this in the Admin; create a question whose date lies in the future;
|
||||
you'll see that the ``Question`` change list claims it was published recently.
|
||||
|
||||
You can also see this using the shell::
|
||||
|
||||
>>> import datetime
|
||||
>>> from django.utils import timezone
|
||||
>>> from polls.models import Poll
|
||||
>>> # create a Poll instance with pub_date 30 days in the future
|
||||
>>> future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
|
||||
>>> from polls.models import Question
|
||||
>>> # create a Question instance with pub_date 30 days in the future
|
||||
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
|
||||
>>> # was it published recently?
|
||||
>>> future_poll.was_published_recently()
|
||||
>>> future_question.was_published_recently()
|
||||
True
|
||||
|
||||
Since things in the future are not 'recent', this is clearly wrong.
|
||||
|
@ -167,20 +167,21 @@ Put the following in the ``tests.py`` file in the ``polls`` application::
|
|||
from django.utils import timezone
|
||||
from django.test import TestCase
|
||||
|
||||
from polls.models import Poll
|
||||
from polls.models import Question
|
||||
|
||||
class PollMethodTests(TestCase):
|
||||
class QuestionMethodTests(TestCase):
|
||||
|
||||
def test_was_published_recently_with_future_poll(self):
|
||||
def test_was_published_recently_with_future_question(self):
|
||||
"""
|
||||
was_published_recently() should return False for polls whose
|
||||
was_published_recently() should return False for questions whose
|
||||
pub_date is in the future
|
||||
"""
|
||||
future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
|
||||
self.assertEqual(future_poll.was_published_recently(), False)
|
||||
time = timezone.now() + datetime.timedelta(days=30)
|
||||
future_question = Question(pub_date=time)
|
||||
self.assertEqual(future_question.was_published_recently(), False)
|
||||
|
||||
What we have done here is created a :class:`django.test.TestCase` subclass
|
||||
with a method that creates a ``Poll`` instance with a ``pub_date`` in the
|
||||
with a method that creates a ``Question`` instance with a ``pub_date`` in the
|
||||
future. We then check the output of ``was_published_recently()`` - which
|
||||
*ought* to be False.
|
||||
|
||||
|
@ -196,11 +197,11 @@ and you'll see something like::
|
|||
Creating test database for alias 'default'...
|
||||
F
|
||||
======================================================================
|
||||
FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests)
|
||||
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests)
|
||||
----------------------------------------------------------------------
|
||||
Traceback (most recent call last):
|
||||
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll
|
||||
self.assertEqual(future_poll.was_published_recently(), False)
|
||||
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
|
||||
self.assertEqual(future_question.was_published_recently(), False)
|
||||
AssertionError: True != False
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
@ -219,7 +220,7 @@ What happened is this:
|
|||
|
||||
* it looked for test methods - ones whose names begin with ``test``
|
||||
|
||||
* in ``test_was_published_recently_with_future_poll`` it created a ``Poll``
|
||||
* in ``test_was_published_recently_with_future_question`` it created a ``Question``
|
||||
instance whose ``pub_date`` field is 30 days in the future
|
||||
|
||||
* ... and using the ``assertEqual()`` method, it discovered that its
|
||||
|
@ -232,14 +233,14 @@ occurred.
|
|||
Fixing the bug
|
||||
--------------
|
||||
|
||||
We already know what the problem is: ``Poll.was_published_recently()`` should
|
||||
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::
|
||||
|
||||
def was_published_recently(self):
|
||||
now = timezone.now()
|
||||
return now - datetime.timedelta(days=1) <= self.pub_date < now
|
||||
return now - datetime.timedelta(days=1) <= self.pub_date < now
|
||||
|
||||
and run the test again::
|
||||
|
||||
|
@ -269,24 +270,26 @@ introduced another.
|
|||
Add two more test methods to the same class, to test the behavior of the method
|
||||
more comprehensively::
|
||||
|
||||
def test_was_published_recently_with_old_poll(self):
|
||||
def test_was_published_recently_with_old_question(self):
|
||||
"""
|
||||
was_published_recently() should return False for polls whose pub_date
|
||||
is older than 1 day
|
||||
was_published_recently() should return False for questions whose
|
||||
pub_date is older than 1 day
|
||||
"""
|
||||
old_poll = Poll(pub_date=timezone.now() - datetime.timedelta(days=30))
|
||||
self.assertEqual(old_poll.was_published_recently(), False)
|
||||
time = timezone.now() - datetime.timedelta(days=30)
|
||||
old_question = Question(pub_date=time)
|
||||
self.assertEqual(old_question.was_published_recently(), False)
|
||||
|
||||
def test_was_published_recently_with_recent_poll(self):
|
||||
def test_was_published_recently_with_recent_question(self):
|
||||
"""
|
||||
was_published_recently() should return True for polls whose pub_date
|
||||
is within the last day
|
||||
was_published_recently() should return True for questions whose
|
||||
pub_date is within the last day
|
||||
"""
|
||||
recent_poll = Poll(pub_date=timezone.now() - datetime.timedelta(hours=1))
|
||||
self.assertEqual(recent_poll.was_published_recently(), True)
|
||||
time = timezone.now() - datetime.timedelta(hours=1)
|
||||
recent_question = Question(pub_date=time)
|
||||
self.assertEqual(recent_question.was_published_recently(), True)
|
||||
|
||||
And now we have three tests that confirm that ``Poll.was_published_recently()``
|
||||
returns sensible values for past, recent, and future polls.
|
||||
And now we have three tests that confirm that ``Question.was_published_recently()``
|
||||
returns sensible values for past, recent, and future questions.
|
||||
|
||||
Again, ``polls`` is a simple application, but however complex it grows in the
|
||||
future and whatever other code it interacts with, we now have some guarantee
|
||||
|
@ -295,9 +298,9 @@ that the method we have written tests for will behave in expected ways.
|
|||
Test a view
|
||||
===========
|
||||
|
||||
The polls application is fairly undiscriminating: it will publish any poll,
|
||||
The polls application is fairly undiscriminating: it will publish any question,
|
||||
including ones whose ``pub_date`` field lies in the future. We should improve
|
||||
this. Setting a ``pub_date`` in the future should mean that the Poll is
|
||||
this. Setting a ``pub_date`` in the future should mean that the Question is
|
||||
published at that moment, but invisible until then.
|
||||
|
||||
A test for a view
|
||||
|
@ -332,7 +335,7 @@ which will allow us to examine some additional attributes on responses such as
|
|||
``response.context`` that otherwise wouldn't be available. Note that this
|
||||
method *does not* setup a test database, so the following will be run against
|
||||
the existing database and the output may differ slightly depending on what
|
||||
polls you already created.
|
||||
questions you already created.
|
||||
|
||||
Next we need to import the test client class (later in ``tests.py`` we will use
|
||||
the :class:`django.test.TestCase` class, which comes with its own client, so
|
||||
|
@ -360,17 +363,17 @@ With that ready, we can ask the client to do some work for us::
|
|||
>>> # note - you might get unexpected results if your ``TIME_ZONE``
|
||||
>>> # in ``settings.py`` is not correct. If you need to change it,
|
||||
>>> # you will also need to restart your shell session
|
||||
>>> from polls.models import Poll
|
||||
>>> from polls.models import Question
|
||||
>>> from django.utils import timezone
|
||||
>>> # create a Poll and save it
|
||||
>>> p = Poll(question="Who is your favorite Beatle?", pub_date=timezone.now())
|
||||
>>> p.save()
|
||||
>>> # create a Question and save it
|
||||
>>> q = Question(question_text="Who is your favorite Beatle?", pub_date=timezone.now())
|
||||
>>> q.save()
|
||||
>>> # check the response once again
|
||||
>>> response = client.get('/polls/')
|
||||
>>> response.content
|
||||
'\n\n\n <ul>\n \n <li><a href="/polls/1/">Who is your favorite Beatle?</a></li>\n \n </ul>\n\n'
|
||||
>>> response.context['latest_poll_list']
|
||||
[<Poll: Who is your favorite Beatle?>]
|
||||
>>> response.context['latest_question_list']
|
||||
[<Question: Who is your favorite Beatle?>]
|
||||
|
||||
Improving our view
|
||||
------------------
|
||||
|
@ -383,13 +386,13 @@ based on :class:`~django.views.generic.list.ListView`::
|
|||
|
||||
class IndexView(generic.ListView):
|
||||
template_name = 'polls/index.html'
|
||||
context_object_name = 'latest_poll_list'
|
||||
context_object_name = 'latest_question_list'
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the last five published polls."""
|
||||
return Poll.objects.order_by('-pub_date')[:5]
|
||||
"""Return the last five published questions."""
|
||||
return Question.objects.order_by('-pub_date')[:5]
|
||||
|
||||
``response.context_data['latest_poll_list']`` extracts the data this view
|
||||
``response.context_data['latest_question_list']`` extracts the data this view
|
||||
places into the context.
|
||||
|
||||
We need to amend the ``get_queryset`` method and change it so that it also
|
||||
|
@ -402,24 +405,24 @@ and then we must amend the ``get_queryset`` method like so::
|
|||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Return the last five published polls (not including those set to be
|
||||
Return the last five published questions (not including those set to be
|
||||
published in the future).
|
||||
"""
|
||||
return Poll.objects.filter(
|
||||
return Question.objects.filter(
|
||||
pub_date__lte=timezone.now()
|
||||
).order_by('-pub_date')[:5]
|
||||
|
||||
``Poll.objects.filter(pub_date__lte=timezone.now())`` returns a queryset
|
||||
containing Polls whose ``pub_date`` is less than or equal to - that is, earlier
|
||||
than or equal to - ``timezone.now``.
|
||||
``Question.objects.filter(pub_date__lte=timezone.now())`` returns a queryset
|
||||
containing ``Question``\s whose ``pub_date`` is less than or equal to - that
|
||||
is, earlier than or equal to - ``timezone.now``.
|
||||
|
||||
Testing our new view
|
||||
--------------------
|
||||
|
||||
Now you can satisfy yourself that this behaves as expected by firing up the
|
||||
runserver, loading the site in your browser, creating ``Polls`` with dates in
|
||||
the past and future, and checking that only those that have been published are
|
||||
listed. You don't want to have to do that *every single time you make any
|
||||
runserver, loading the site in your browser, creating ``Questions`` with dates
|
||||
in the past and future, and checking that only those that have been published
|
||||
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
|
||||
shell session above.
|
||||
|
||||
|
@ -427,91 +430,98 @@ Add the following to ``polls/tests.py``::
|
|||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
and we'll create a factory method to create polls as well as a new test class::
|
||||
and we'll create a factory method to create questions as well as a new test
|
||||
class::
|
||||
|
||||
def create_poll(question, days):
|
||||
def create_question(question_text, days):
|
||||
"""
|
||||
Creates a poll with the given `question` published the given number of
|
||||
`days` offset to now (negative for polls published in the past,
|
||||
positive for polls that have yet to be published).
|
||||
Creates a question with the given `question_text` published the given
|
||||
number of `days` offset to now (negative for questions published
|
||||
in the past, positive for questions that have yet to be published).
|
||||
"""
|
||||
return Poll.objects.create(question=question,
|
||||
pub_date=timezone.now() + datetime.timedelta(days=days))
|
||||
time = timezone.now() + datetime.timedelta(days=days)
|
||||
return Question.objects.create(question_text=question_text,
|
||||
pub_date=time)
|
||||
|
||||
class PollViewTests(TestCase):
|
||||
def test_index_view_with_no_polls(self):
|
||||
|
||||
class QuestionViewTests(TestCase):
|
||||
def test_index_view_with_no_questions(self):
|
||||
"""
|
||||
If no polls exist, an appropriate message should be displayed.
|
||||
If no questions exist, an appropriate message should be displayed.
|
||||
"""
|
||||
response = self.client.get(reverse('polls:index'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "No polls are available.")
|
||||
self.assertQuerysetEqual(response.context['latest_poll_list'], [])
|
||||
self.assertQuerysetEqual(response.context['latest_question_list'], [])
|
||||
|
||||
def test_index_view_with_a_past_poll(self):
|
||||
def test_index_view_with_a_past_question(self):
|
||||
"""
|
||||
Polls with a pub_date in the past should be displayed on the index page.
|
||||
Questions with a pub_date in the past should be displayed on the
|
||||
index page
|
||||
"""
|
||||
create_poll(question="Past poll.", days=-30)
|
||||
create_question(question_text="Past question.", days=-30)
|
||||
response = self.client.get(reverse('polls:index'))
|
||||
self.assertQuerysetEqual(
|
||||
response.context['latest_poll_list'],
|
||||
['<Poll: Past poll.>']
|
||||
response.context['latest_question_list'],
|
||||
['<Question: Past question.>']
|
||||
)
|
||||
|
||||
def test_index_view_with_a_future_poll(self):
|
||||
def test_index_view_with_a_future_question(self):
|
||||
"""
|
||||
Polls with a pub_date in the future should not be displayed on the
|
||||
index page.
|
||||
Questions with a pub_date in the future should not be displayed on
|
||||
the index page.
|
||||
"""
|
||||
create_poll(question="Future poll.", days=30)
|
||||
create_question(question_text="Future question.", days=30)
|
||||
response = self.client.get(reverse('polls:index'))
|
||||
self.assertContains(response, "No polls are available.", status_code=200)
|
||||
self.assertQuerysetEqual(response.context['latest_poll_list'], [])
|
||||
self.assertContains(response, "No polls are available.",
|
||||
status_code=200)
|
||||
self.assertQuerysetEqual(response.context['latest_question_list'], [])
|
||||
|
||||
def test_index_view_with_future_poll_and_past_poll(self):
|
||||
def test_index_view_with_future_question_and_past_question(self):
|
||||
"""
|
||||
Even if both past and future polls exist, only past polls should be
|
||||
displayed.
|
||||
Even if both past and future questions exist, only past questions
|
||||
should be displayed.
|
||||
"""
|
||||
create_poll(question="Past poll.", days=-30)
|
||||
create_poll(question="Future poll.", days=30)
|
||||
create_question(question_text="Past question.", days=-30)
|
||||
create_question(question_text="Future question.", days=30)
|
||||
response = self.client.get(reverse('polls:index'))
|
||||
self.assertQuerysetEqual(
|
||||
response.context['latest_poll_list'],
|
||||
['<Poll: Past poll.>']
|
||||
response.context['latest_question_list'],
|
||||
['<Question: Past question.>']
|
||||
)
|
||||
|
||||
def test_index_view_with_two_past_polls(self):
|
||||
def test_index_view_with_two_past_questions(self):
|
||||
"""
|
||||
The polls index page may display multiple polls.
|
||||
The questions index page may display multiple questions.
|
||||
"""
|
||||
create_poll(question="Past poll 1.", days=-30)
|
||||
create_poll(question="Past poll 2.", days=-5)
|
||||
create_question(question_text="Past quesiton 1.", days=-30)
|
||||
create_question(question_text="Past question 2.", days=-5)
|
||||
response = self.client.get(reverse('polls:index'))
|
||||
self.assertQuerysetEqual(
|
||||
response.context['latest_poll_list'],
|
||||
['<Poll: Past poll 2.>', '<Poll: Past poll 1.>']
|
||||
response.context['latest_question_list'],
|
||||
['<Question: Past question 2.>', '<Question: Past question 1.>']
|
||||
)
|
||||
|
||||
|
||||
Let's look at some of these more closely.
|
||||
|
||||
First is a poll factory method, ``create_poll``, to take some repetition out
|
||||
of the process of creating polls.
|
||||
First is a question factory method, ``create_question``, to take some
|
||||
repetition out of the process of creating questions.
|
||||
|
||||
``test_index_view_with_no_polls`` doesn't create any polls, but checks the
|
||||
message: "No polls are available." and verifies the ``latest_poll_list`` is
|
||||
empty. Note that the :class:`django.test.TestCase` class provides some
|
||||
``test_index_view_with_no_questions`` doesn't create any questions, but checks
|
||||
the message: "No polls are available." and verifies the ``latest_question_list``
|
||||
is empty. Note that the :class:`django.test.TestCase` class provides some
|
||||
additional assertion methods. In these examples, we use
|
||||
:meth:`~django.test.SimpleTestCase.assertContains()` and
|
||||
:meth:`~django.test.TransactionTestCase.assertQuerysetEqual()`.
|
||||
|
||||
In ``test_index_view_with_a_past_poll``, we create a poll and verify that it
|
||||
In ``test_index_view_with_a_past_question``, we create a question and verify that it
|
||||
appears in the list.
|
||||
|
||||
In ``test_index_view_with_a_future_poll``, we create a poll with a ``pub_date``
|
||||
in the future. The database is reset for each test method, so the first poll is
|
||||
no longer there, and so again the index shouldn't have any polls in it.
|
||||
In ``test_index_view_with_a_future_question``, we create a question with a
|
||||
``pub_date`` in the future. The database is reset for each test method, so the
|
||||
first question is no longer there, and so again the index shouldn't have any
|
||||
questions in it.
|
||||
|
||||
And so on. In effect, we are using the tests to tell a story of admin input
|
||||
and user experience on the site, and checking that at every state and for every
|
||||
|
@ -520,41 +530,47 @@ new change in the state of the system, the expected results are published.
|
|||
Testing the ``DetailView``
|
||||
--------------------------
|
||||
|
||||
What we have works well; however, even though future polls 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``::
|
||||
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``::
|
||||
|
||||
|
||||
class DetailView(generic.DetailView):
|
||||
...
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Excludes any polls that aren't published yet.
|
||||
Excludes any questions that aren't published yet.
|
||||
"""
|
||||
return Poll.objects.filter(pub_date__lte=timezone.now())
|
||||
return Question.objects.filter(pub_date__lte=timezone.now())
|
||||
|
||||
And of course, we will add some tests, to check that a ``Poll`` whose
|
||||
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::
|
||||
|
||||
class PollIndexDetailTests(TestCase):
|
||||
def test_detail_view_with_a_future_poll(self):
|
||||
class QuestionIndexDetailTests(TestCase):
|
||||
def test_detail_view_with_a_future_question(self):
|
||||
"""
|
||||
The detail view of a poll with a pub_date in the future should
|
||||
The detail view of a question with a pub_date in the future should
|
||||
return a 404 not found.
|
||||
"""
|
||||
future_poll = create_poll(question='Future poll.', days=5)
|
||||
response = self.client.get(reverse('polls:detail', args=(future_poll.id,)))
|
||||
future_question = create_question(question_text='Future question.',
|
||||
days=5)
|
||||
response = self.client.get(reverse('polls:detail',
|
||||
args=(future_question.id,)))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_detail_view_with_a_past_poll(self):
|
||||
def test_detail_view_with_a_past_question(self):
|
||||
"""
|
||||
The detail view of a poll with a pub_date in the past should display
|
||||
the poll's question.
|
||||
The detail view of a question with a pub_date in the past should
|
||||
display the question's text.
|
||||
"""
|
||||
past_poll = create_poll(question='Past Poll.', days=-5)
|
||||
response = self.client.get(reverse('polls:detail', args=(past_poll.id,)))
|
||||
self.assertContains(response, past_poll.question, status_code=200)
|
||||
past_question = create_question(question_text='Past Question.',
|
||||
days=-5)
|
||||
response = self.client.get(reverse('polls:detail',
|
||||
args=(past_question.id,)))
|
||||
self.assertContains(response, past_question.question_text,
|
||||
status_code=200)
|
||||
|
||||
|
||||
Ideas for more tests
|
||||
--------------------
|
||||
|
@ -564,17 +580,17 @@ create a new test class for that view. It'll be very similar to what we have
|
|||
just created; in fact there will be a lot of repetition.
|
||||
|
||||
We could also improve our application in other ways, adding tests along the
|
||||
way. For example, it's silly that ``Polls`` can be published on the site that
|
||||
have no ``Choices``. So, our views could check for this, and exclude such
|
||||
``Polls``. Our tests would create a ``Poll`` without ``Choices`` and then test
|
||||
that it's not published, as well as create a similar ``Poll`` *with*
|
||||
``Choices``, and test that it *is* published.
|
||||
way. For example, it's silly that ``Questions`` can be published on the site
|
||||
that have no ``Choices``. So, our views could check for this, and exclude such
|
||||
``Questions``. Our tests would create a ``Question`` without ``Choices`` and
|
||||
then test that it's not published, as well as create a similar ``Question``
|
||||
*with* ``Choices``, and test that it *is* published.
|
||||
|
||||
Perhaps logged-in admin users should be allowed to see unpublished ``Polls``,
|
||||
but not ordinary visitors. Again: whatever needs to be added to the software to
|
||||
accomplish this should be accompanied by a test, whether you write the test
|
||||
first and then make the code pass the test, or work out the logic in your code
|
||||
first and then write a test to prove it.
|
||||
Perhaps logged-in admin users should be allowed to see unpublished
|
||||
``Questions``, but not ordinary visitors. Again: whatever needs to be added to
|
||||
the software to accomplish this should be accompanied by a test, whether you
|
||||
write the test first and then make the code pass the test, or work out the
|
||||
logic in your code first and then write a test to prove it.
|
||||
|
||||
At a certain point you are bound to look at your tests and wonder whether your
|
||||
code is suffering from test bloat, which brings us to:
|
||||
|
@ -591,7 +607,7 @@ once and then forget about it. It will continue performing its useful function
|
|||
as you continue to develop your program.
|
||||
|
||||
Sometimes tests will need to be updated. Suppose that we amend our views so that
|
||||
only ``Polls`` with ``Choices`` are published. In that case, many of our
|
||||
only ``Questions`` with ``Choices`` are published. In that case, many of our
|
||||
existing tests will fail - *telling us exactly which tests need to be amended to
|
||||
bring them up to date*, so to that extent tests help look after themselves.
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ template tag from the ``staticfiles`` template library. The ``{% static %}``
|
|||
template tag generates the absolute URL of the static file.
|
||||
|
||||
That's all you need to do for development. Reload
|
||||
``http://localhost:8000/polls/`` and you should see that the poll links are
|
||||
``http://localhost:8000/polls/`` and you should see that the question links are
|
||||
green (Django style!) which means that your stylesheet was properly loaded.
|
||||
|
||||
Adding a background-image
|
||||
|
|