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
|
the :ref:`DRY Principle <dry>`. The goal is to define your data model in one
|
||||||
place and automatically derive things from it.
|
place and automatically derive things from it.
|
||||||
|
|
||||||
In our simple poll app, we'll create two models: ``Poll`` and ``Choice``.
|
In our simple poll app, we'll create two models: ``Question`` and ``Choice``.
|
||||||
A ``Poll`` has a question and a publication date. A ``Choice`` has two fields:
|
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
|
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
|
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::
|
||||||
|
|
||||||
from django.db import models
|
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')
|
pub_date = models.DateTimeField('date published')
|
||||||
|
|
||||||
|
|
||||||
class Choice(models.Model):
|
class Choice(models.Model):
|
||||||
poll = models.ForeignKey(Poll)
|
question = models.ForeignKey(Question)
|
||||||
choice_text = models.CharField(max_length=200)
|
choice_text = models.CharField(max_length=200)
|
||||||
votes = models.IntegerField(default=0)
|
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
|
:class:`~django.db.models.DateTimeField` for datetimes. This tells Django what
|
||||||
type of data each field holds.
|
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
|
``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.
|
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
|
: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.
|
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
|
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
|
other fields in this model, the field's machine-readable name will suffice as
|
||||||
its human-readable name.
|
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
|
Finally, note a relationship is defined, using
|
||||||
:class:`~django.db.models.ForeignKey`. That tells Django each ``Choice`` is related
|
: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.
|
many-to-ones, many-to-manys and one-to-ones.
|
||||||
|
|
||||||
.. _`Python path`: http://docs.python.org/tutorial/modules.html#the-module-search-path
|
.. _`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:
|
is able to:
|
||||||
|
|
||||||
* Create a database schema (``CREATE TABLE`` statements) for this app.
|
* 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.
|
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
|
.. code-block:: sql
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
CREATE TABLE "polls_poll" (
|
CREATE TABLE "polls_question" (
|
||||||
"id" integer NOT NULL PRIMARY KEY,
|
"id" integer NOT NULL PRIMARY KEY,
|
||||||
"question" varchar(200) NOT NULL,
|
"question_text" varchar(200) NOT NULL,
|
||||||
"pub_date" datetime NOT NULL
|
"pub_date" datetime NOT NULL
|
||||||
);
|
);
|
||||||
CREATE TABLE "polls_choice" (
|
CREATE TABLE "polls_choice" (
|
||||||
"id" integer NOT NULL PRIMARY KEY,
|
"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,
|
"choice_text" varchar(200) NOT NULL,
|
||||||
"votes" integer NOT NULL
|
"votes" integer NOT NULL
|
||||||
);
|
);
|
||||||
|
@ -449,7 +451,7 @@ Note the following:
|
||||||
example above is generated for SQLite.
|
example above is generated for SQLite.
|
||||||
|
|
||||||
* Table names are automatically generated by combining the name of the app
|
* 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.)
|
``choice``. (You can override this behavior.)
|
||||||
|
|
||||||
* Primary keys (IDs) are added automatically. (You can override this, too.)
|
* 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>`::
|
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.
|
# No questions are in the system yet.
|
||||||
>>> Poll.objects.all()
|
>>> Question.objects.all()
|
||||||
[]
|
[]
|
||||||
|
|
||||||
# Create a new Poll.
|
# Create a new Question.
|
||||||
# Support for time zones is enabled in the default settings file, so
|
# Support for time zones is enabled in the default settings file, so
|
||||||
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
|
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
|
||||||
# instead of datetime.datetime.now() and it will do the right thing.
|
# instead of datetime.datetime.now() and it will do the right thing.
|
||||||
>>> from django.utils import timezone
|
>>> 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.
|
# 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
|
# 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
|
# on which database you're using. That's no biggie; it just means your
|
||||||
# database backend prefers to return integers as Python long integer
|
# database backend prefers to return integers as Python long integer
|
||||||
# objects.
|
# objects.
|
||||||
>>> p.id
|
>>> q.id
|
||||||
1
|
1
|
||||||
|
|
||||||
# Access database columns via Python attributes.
|
# Access database columns via Python attributes.
|
||||||
>>> p.question
|
>>> q.question_text
|
||||||
"What's new?"
|
"What's new?"
|
||||||
>>> p.pub_date
|
>>> q.pub_date
|
||||||
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)
|
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)
|
||||||
|
|
||||||
# Change values by changing the attributes, then calling save().
|
# Change values by changing the attributes, then calling save().
|
||||||
>>> p.question = "What's up?"
|
>>> q.question_text = "What's up?"
|
||||||
>>> p.save()
|
>>> q.save()
|
||||||
|
|
||||||
# objects.all() displays all the polls in the database.
|
# objects.all() displays all the questions in the database.
|
||||||
>>> Poll.objects.all()
|
>>> Question.objects.all()
|
||||||
[<Poll: Poll object>]
|
[<Question: Question object>]
|
||||||
|
|
||||||
|
|
||||||
Wait a minute. ``<Poll: Poll object>`` is, utterly, an unhelpful representation
|
Wait a minute. ``<Question: Question object>`` is, utterly, an unhelpful representation
|
||||||
of this object. Let's fix that by editing the polls model (in the
|
of this object. Let's fix that by editing the ``Question`` model (in the
|
||||||
``polls/models.py`` file) and adding a
|
``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
|
``Choice``. On Python 3, simply replace ``__unicode__`` by ``__str__`` in the
|
||||||
following example::
|
following example::
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
class Poll(models.Model):
|
class Question(models.Model):
|
||||||
# ...
|
# ...
|
||||||
def __unicode__(self): # Python 3: def __str__(self):
|
def __unicode__(self): # Python 3: def __str__(self):
|
||||||
return self.question
|
return self.question_text
|
||||||
|
|
||||||
class Choice(models.Model):
|
class Choice(models.Model):
|
||||||
# ...
|
# ...
|
||||||
|
@ -629,7 +631,7 @@ demonstration::
|
||||||
import datetime
|
import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
# ...
|
# ...
|
||||||
class Poll(models.Model):
|
class Question(models.Model):
|
||||||
# ...
|
# ...
|
||||||
def was_published_recently(self):
|
def was_published_recently(self):
|
||||||
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
|
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
|
Save these changes and start a new Python interactive shell by running
|
||||||
``python manage.py shell`` again::
|
``python manage.py shell`` again::
|
||||||
|
|
||||||
>>> from polls.models import Poll, Choice
|
>>> from polls.models import Question, Choice
|
||||||
|
|
||||||
# Make sure our __unicode__() addition worked.
|
# Make sure our __unicode__() addition worked.
|
||||||
>>> Poll.objects.all()
|
>>> Question.objects.all()
|
||||||
[<Poll: What's up?>]
|
[<Question: What's up?>]
|
||||||
|
|
||||||
# Django provides a rich database lookup API that's entirely driven by
|
# Django provides a rich database lookup API that's entirely driven by
|
||||||
# keyword arguments.
|
# keyword arguments.
|
||||||
>>> Poll.objects.filter(id=1)
|
>>> Question.objects.filter(id=1)
|
||||||
[<Poll: What's up?>]
|
[<Question: What's up?>]
|
||||||
>>> Poll.objects.filter(question__startswith='What')
|
>>> Question.objects.filter(question_text__startswith='What')
|
||||||
[<Poll: What's up?>]
|
[<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
|
>>> from django.utils import timezone
|
||||||
>>> current_year = timezone.now().year
|
>>> current_year = timezone.now().year
|
||||||
>>> Poll.objects.get(pub_date__year=current_year)
|
>>> Question.objects.get(pub_date__year=current_year)
|
||||||
<Poll: What's up?>
|
<Question: What's up?>
|
||||||
|
|
||||||
# Request an ID that doesn't exist, this will raise an exception.
|
# 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):
|
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
|
# Lookup by a primary key is the most common case, so Django provides a
|
||||||
# shortcut for primary-key exact lookups.
|
# shortcut for primary-key exact lookups.
|
||||||
# The following is identical to Poll.objects.get(id=1).
|
# The following is identical to Question.objects.get(id=1).
|
||||||
>>> Poll.objects.get(pk=1)
|
>>> Question.objects.get(pk=1)
|
||||||
<Poll: What's up?>
|
<Question: What's up?>
|
||||||
|
|
||||||
# Make sure our custom method worked.
|
# Make sure our custom method worked.
|
||||||
>>> p = Poll.objects.get(pk=1)
|
>>> q = Question.objects.get(pk=1)
|
||||||
>>> p.was_published_recently()
|
>>> q.was_published_recently()
|
||||||
True
|
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
|
# Choice object, does the INSERT statement, adds the choice to the set
|
||||||
# of available choices and returns the new Choice object. Django creates
|
# of available choices and returns the new Choice object. Django creates
|
||||||
# a set to hold the "other side" of a ForeignKey relation
|
# a set to hold the "other side" of a ForeignKey relation
|
||||||
# (e.g. a poll's choices) which can be accessed via the API.
|
# (e.g. a question's choice) which can be accessed via the API.
|
||||||
>>> p = Poll.objects.get(pk=1)
|
>>> q = Question.objects.get(pk=1)
|
||||||
|
|
||||||
# Display any choices from the related object set -- none so far.
|
# Display any choices from the related object set -- none so far.
|
||||||
>>> p.choice_set.all()
|
>>> q.choice_set.all()
|
||||||
[]
|
[]
|
||||||
|
|
||||||
# Create three choices.
|
# 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>
|
<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>
|
<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.
|
# Choice objects have API access to their related Question objects.
|
||||||
>>> c.poll
|
>>> c.question
|
||||||
<Poll: What's up?>
|
<Question: What's up?>
|
||||||
|
|
||||||
# And vice versa: Poll objects get access to Choice objects.
|
# And vice versa: Question objects get access to Choice objects.
|
||||||
>>> p.choice_set.all()
|
>>> q.choice_set.all()
|
||||||
[<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]
|
[<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]
|
||||||
>>> p.choice_set.count()
|
>>> q.choice_set.count()
|
||||||
3
|
3
|
||||||
|
|
||||||
# The API automatically follows relationships as far as you need.
|
# The API automatically follows relationships as far as you need.
|
||||||
# Use double underscores to separate relationships.
|
# Use double underscores to separate relationships.
|
||||||
# This works as many levels deep as you want; there's no limit.
|
# 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).
|
# (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>]
|
[<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]
|
||||||
|
|
||||||
# Let's delete one of the choices. Use delete() for that.
|
# 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()
|
>>> c.delete()
|
||||||
|
|
||||||
For more information on model relations, see :doc:`Accessing related objects
|
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:
|
You should see the Django admin index page:
|
||||||
|
|
||||||
.. image:: _images/admin02t.png
|
.. image:: _images/admin02.png
|
||||||
:alt: Django admin index page
|
:alt: Django admin index page
|
||||||
|
|
||||||
You should see a few types of editable content: groups and users. They are
|
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.
|
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`
|
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::
|
||||||
|
|
||||||
from django.contrib import admin
|
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
|
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:
|
the admin index page:
|
||||||
|
|
||||||
.. image:: _images/admin03t.png
|
.. image:: _images/admin03t.png
|
||||||
:alt: Django admin index page, now with polls displayed
|
:alt: Django admin index page, now with polls displayed
|
||||||
|
|
||||||
Click "Polls." Now you're at the "change list" page for polls. This page
|
Click "Questions". Now you're at the "change list" page for questions. This page
|
||||||
displays all the polls in the database and lets you choose one to change it.
|
displays all the question in the database and lets you choose one to change it.
|
||||||
There's the "What's up?" poll we created in the first tutorial:
|
There's the "What's up?" question we created in the first tutorial:
|
||||||
|
|
||||||
.. image:: _images/admin04t.png
|
.. image:: _images/admin04t.png
|
||||||
:alt: Polls change list page
|
: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
|
.. image:: _images/admin05t.png
|
||||||
:alt: Editing form for poll object
|
:alt: Editing form for question object
|
||||||
|
|
||||||
Things to note here:
|
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`,
|
* The different model field types (:class:`~django.db.models.DateTimeField`,
|
||||||
:class:`~django.db.models.CharField`) correspond to the appropriate HTML
|
: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.
|
* Delete -- Displays a delete confirmation page.
|
||||||
|
|
||||||
If the value of "Date published" doesn't match the time when you created the
|
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 :setting:`TIME_ZONE` setting. Change it, reload the page and check that
|
||||||
the correct value appears.
|
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:
|
with the timestamp and username of the person who made the change:
|
||||||
|
|
||||||
.. image:: _images/admin06t.png
|
.. image:: _images/admin06t.png
|
||||||
:alt: History page for poll object
|
:alt: History page for question object
|
||||||
|
|
||||||
Customize the admin form
|
Customize the admin form
|
||||||
========================
|
========================
|
||||||
|
|
||||||
Take a few minutes to marvel at all the code you didn't have to write. By
|
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
|
registering the ``Question`` model with ``admin.site.register(Question)``,
|
||||||
to construct a default form representation. Often, you'll want to customize how
|
Django was able to construct a default form representation. Often, you'll want
|
||||||
the admin form looks and works. You'll do this by telling Django the options
|
to customize how the admin form looks and works. You'll do this by telling
|
||||||
you want when you register the object.
|
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
|
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 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
|
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
|
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::
|
up into fieldsets::
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from polls.models import Poll
|
from polls.models import Question
|
||||||
|
|
||||||
class PollAdmin(admin.ModelAdmin):
|
|
||||||
|
class QuestionAdmin(admin.ModelAdmin):
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {'fields': ['question']}),
|
(None, {'fields': ['question_text']}),
|
||||||
('Date information', {'fields': ['pub_date']}),
|
('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.
|
The first element of each tuple in ``fieldsets`` is the title of the fieldset.
|
||||||
Here's what our form looks like now:
|
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::
|
aren't commonly used::
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from polls.models import Poll
|
from polls.models import Question
|
||||||
|
|
||||||
class PollAdmin(admin.ModelAdmin):
|
|
||||||
|
class QuestionAdmin(admin.ModelAdmin):
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {'fields': ['question']}),
|
(None, {'fields': ['question_text']}),
|
||||||
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
|
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -219,13 +222,13 @@ aren't commonly used::
|
||||||
Adding related objects
|
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.
|
the admin page doesn't display choices.
|
||||||
|
|
||||||
Yet.
|
Yet.
|
||||||
|
|
||||||
There are two ways to solve this problem. The first is to register ``Choice``
|
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 django.contrib import admin
|
||||||
from polls.models import Choice
|
from polls.models import Choice
|
||||||
|
@ -238,48 +241,51 @@ looks like this:
|
||||||
.. image:: _images/admin10.png
|
.. image:: _images/admin10.png
|
||||||
:alt: Choice admin page
|
: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
|
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.
|
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
|
``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
|
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 poll to the database and
|
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
|
dynamically add it as the selected choice on the "Add choice" form you're
|
||||||
looking at.
|
looking at.
|
||||||
|
|
||||||
But, really, this is an inefficient way of adding ``Choice`` objects to the system.
|
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
|
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::
|
registration code to read::
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from polls.models import Choice, Poll
|
from polls.models import Choice, Question
|
||||||
|
|
||||||
|
|
||||||
class ChoiceInline(admin.StackedInline):
|
class ChoiceInline(admin.StackedInline):
|
||||||
model = Choice
|
model = Choice
|
||||||
extra = 3
|
extra = 3
|
||||||
|
|
||||||
class PollAdmin(admin.ModelAdmin):
|
|
||||||
|
class QuestionAdmin(admin.ModelAdmin):
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {'fields': ['question']}),
|
(None, {'fields': ['question_text']}),
|
||||||
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
|
('Date information', {'fields': ['pub_date'],
|
||||||
|
'classes': ['collapse']}),
|
||||||
]
|
]
|
||||||
inlines = [ChoiceInline]
|
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."
|
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
|
.. 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
|
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
|
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:
|
related objects are displayed in a more compact, table-based format:
|
||||||
|
|
||||||
.. image:: _images/admin12t.png
|
.. 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
|
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.
|
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
|
Customize the admin change list
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
Now that the Poll admin page is looking good, let's make some tweaks to the
|
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 polls in the system.
|
"change list" page -- the one that displays all the questions in the system.
|
||||||
|
|
||||||
Here's what it looks like at this point:
|
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
|
``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::
|
||||||
|
|
||||||
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
|
Just for good measure, let's also include the ``was_published_recently`` custom
|
||||||
method from Tutorial 1::
|
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
|
.. image:: _images/admin13t.png
|
||||||
:alt: Polls change list page, updated
|
: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
|
You can improve that by giving that method (in :file:`polls/models.py`) a few
|
||||||
attributes, as follows::
|
attributes, as follows::
|
||||||
|
|
||||||
class Poll(models.Model):
|
class Question(models.Model):
|
||||||
# ...
|
# ...
|
||||||
def was_published_recently(self):
|
def was_published_recently(self):
|
||||||
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
|
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.boolean = True
|
||||||
was_published_recently.short_description = 'Published recently?'
|
was_published_recently.short_description = 'Published recently?'
|
||||||
|
|
||||||
Edit your :file:`polls/admin.py` file again and add an improvement to the Poll
|
Edit your :file:`polls/admin.py` file again and add an improvement to the Question
|
||||||
change list page: Filters. Add the following line to ``PollAdmin``::
|
change list page: Filters. Add the following line to ``QuestionAdmin``::
|
||||||
|
|
||||||
list_filter = ['pub_date']
|
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::
|
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
|
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
|
fields as you'd like -- although because it uses a ``LIKE`` query behind the
|
||||||
scenes, keep it reasonable, to keep your database happy.
|
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:
|
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.
|
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
|
* 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
|
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
|
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
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
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
|
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.
|
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
|
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
|
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.
|
``index`` view.
|
||||||
|
|
||||||
The :func:`~django.conf.urls.url` function is passed four arguments, two
|
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
|
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::
|
||||||
|
|
||||||
def detail(request, poll_id):
|
def detail(request, question_id):
|
||||||
return HttpResponse("You're looking at poll %s." % poll_id)
|
return HttpResponse("You're looking at question %s." % question_id)
|
||||||
|
|
||||||
def results(request, poll_id):
|
def results(request, question_id):
|
||||||
return HttpResponse("You're looking at the results of poll %s." % poll_id)
|
response = "You're looking at the results of question %s."
|
||||||
|
return HttpResponse(response % question_id)
|
||||||
|
|
||||||
def vote(request, poll_id):
|
def vote(request, question_id):
|
||||||
return HttpResponse("You're voting on poll %s." % poll_id)
|
return HttpResponse("You're voting on question %s." % question_id)
|
||||||
|
|
||||||
Wire these new views into the ``polls.urls`` module by adding the following
|
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::
|
||||||
|
@ -193,11 +195,11 @@ Wire these new views into the ``polls.urls`` module by adding the following
|
||||||
# ex: /polls/
|
# ex: /polls/
|
||||||
url(r'^$', views.index, name='index'),
|
url(r'^$', views.index, name='index'),
|
||||||
# ex: /polls/5/
|
# 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/
|
# 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/
|
# 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()``
|
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
|
* Then, Django will strip off the matching text (``"polls/"``) and send the
|
||||||
remaining text -- ``"34/"`` -- to the 'polls.urls' URLconf for
|
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::
|
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
|
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
|
be used to identify the matched pattern; and ``\d+`` is a regular expression to
|
||||||
match a sequence of digits (i.e., a number).
|
match a sequence of digits (i.e., a number).
|
||||||
|
|
||||||
|
@ -271,11 +273,12 @@ commas, according to publication date::
|
||||||
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
from polls.models import Poll
|
from polls.models import Question
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
latest_poll_list = Poll.objects.order_by('-pub_date')[:5]
|
latest_question_list = Question.objects.order_by('-pub_date')[:5]
|
||||||
output = ', '.join([p.question for p in latest_poll_list])
|
output = ', '.join([p.question_text for p in latest_question_list])
|
||||||
return HttpResponse(output)
|
return HttpResponse(output)
|
||||||
|
|
||||||
There's a problem here, though: the page's design is hard-coded in the view. If
|
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
|
.. code-block:: html+django
|
||||||
|
|
||||||
{% if latest_poll_list %}
|
{% if latest_question_list %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for poll in latest_poll_list %}
|
{% for question in latest_question_list %}
|
||||||
<li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
|
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% 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.http import HttpResponse
|
||||||
from django.template import RequestContext, loader
|
from django.template import RequestContext, loader
|
||||||
|
|
||||||
from polls.models import Poll
|
from polls.models import Question
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
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')
|
template = loader.get_template('polls/index.html')
|
||||||
context = RequestContext(request, {
|
context = RequestContext(request, {
|
||||||
'latest_poll_list': latest_poll_list,
|
'latest_question_list': latest_question_list,
|
||||||
})
|
})
|
||||||
return HttpResponse(template.render(context))
|
return HttpResponse(template.render(context))
|
||||||
|
|
||||||
|
@ -356,8 +360,8 @@ context. The context is a dictionary mapping template variable names to Python
|
||||||
objects.
|
objects.
|
||||||
|
|
||||||
Load the page by pointing your browser at "/polls/", and you should see a
|
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
|
bulleted-list containing the "What's up" question from Tutorial 1. The link points
|
||||||
to the poll's detail page.
|
to the question's detail page.
|
||||||
|
|
||||||
A shortcut: :func:`~django.shortcuts.render`
|
A shortcut: :func:`~django.shortcuts.render`
|
||||||
--------------------------------------------
|
--------------------------------------------
|
||||||
|
@ -369,11 +373,12 @@ rewritten::
|
||||||
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
from polls.models import Poll
|
from polls.models import Question
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
|
latest_question_list = Question.objects.all().order_by('-pub_date')[:5]
|
||||||
context = {'latest_poll_list': latest_poll_list}
|
context = {'latest_question_list': latest_question_list}
|
||||||
return render(request, 'polls/index.html', context)
|
return render(request, 'polls/index.html', context)
|
||||||
|
|
||||||
Note that once we've done this in all these views, we no longer need to import
|
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
|
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::
|
for a given poll. Here's the view::
|
||||||
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import render
|
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:
|
try:
|
||||||
poll = Poll.objects.get(pk=poll_id)
|
question = Question.objects.get(pk=question_id)
|
||||||
except Poll.DoesNotExist:
|
except Question.DoesNotExist:
|
||||||
raise Http404
|
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
|
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
|
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
|
later, but if you'd like to quickly get the above example working, a file
|
||||||
containing just::
|
containing just::
|
||||||
|
|
||||||
{{ poll }}
|
{{ question }}
|
||||||
|
|
||||||
will get you started for now.
|
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 django.shortcuts import render, get_object_or_404
|
||||||
|
|
||||||
from polls.models import Poll
|
from polls.models import Question
|
||||||
# ...
|
# ...
|
||||||
def detail(request, poll_id):
|
def detail(request, question_id):
|
||||||
poll = get_object_or_404(Poll, pk=poll_id)
|
question = get_object_or_404(Question, pk=question_id)
|
||||||
return render(request, 'polls/detail.html', {'poll': poll})
|
return render(request, 'polls/detail.html', {'question': question})
|
||||||
|
|
||||||
The :func:`~django.shortcuts.get_object_or_404` function takes a Django model
|
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
|
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
|
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:
|
like:
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
<h1>{{ poll.question }}</h1>
|
<h1>{{ question.question_text }}</h1>
|
||||||
<ul>
|
<ul>
|
||||||
{% for choice in poll.choice_set.all %}
|
{% for choice in question.choice_set.all %}
|
||||||
<li>{{ choice.choice_text }}</li>
|
<li>{{ choice.choice_text }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
The template system uses dot-lookup syntax to access variable attributes. In
|
The template system uses dot-lookup syntax to access variable attributes. In
|
||||||
the example of ``{{ poll.question }}``, first Django does a dictionary lookup
|
the example of ``{{ question.question_text }}``, first Django does a dictionary lookup
|
||||||
on the object ``poll``. Failing that, it tries an attribute lookup -- which
|
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
|
works, in this case. If attribute lookup had failed, it would've tried a
|
||||||
list-index lookup.
|
list-index lookup.
|
||||||
|
|
||||||
Method-calling happens in the :ttag:`{% for %}<for>` loop:
|
Method-calling happens in the :ttag:`{% for %}<for>` loop:
|
||||||
``poll.choice_set.all`` is interpreted as the Python code
|
``question.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()``, which returns an iterable of ``Choice`` objects and is
|
||||||
suitable for use in the :ttag:`{% for %}<for>` tag.
|
suitable for use in the :ttag:`{% for %}<for>` tag.
|
||||||
|
|
||||||
See the :doc:`template guide </topics/templates>` for more about templates.
|
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
|
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:
|
template, the link was partially hardcoded like this:
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. 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
|
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
|
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
|
.. 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::
|
.. note::
|
||||||
|
|
||||||
If ``{% url 'detail' poll.id %}`` (with quotes) doesn't work, but
|
If ``{% url 'detail' question.id %}`` (with quotes) doesn't work, but
|
||||||
``{% url detail poll.id %}`` (without quotes) does, that means you're
|
``{% url detail question.id %}`` (without quotes) does, that means you're
|
||||||
using a version of Django < 1.5. In this case, add the following
|
using a version of Django < 1.5. In this case, add the following
|
||||||
declaration at the top of your template:
|
declaration at the top of your template:
|
||||||
|
|
||||||
|
@ -520,7 +525,7 @@ defined below::
|
||||||
|
|
||||||
...
|
...
|
||||||
# the 'name' value as called by the {% url %} template tag
|
# 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,
|
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'
|
# 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
|
Namespacing URL names
|
||||||
|
@ -560,13 +565,13 @@ Now change your ``polls/index.html`` template from:
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. 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:
|
to point at the namespaced detail view:
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. 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
|
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.
|
</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
|
.. code-block:: html+django
|
||||||
|
|
||||||
<h1>{{ poll.question }}</h1>
|
<h1>{{ question.question_text }}</h1>
|
||||||
|
|
||||||
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
|
{% 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 %}
|
{% 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 }}" />
|
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
|
||||||
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
|
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -29,13 +29,13 @@ tutorial, so that the template contains an HTML ``<form>`` element:
|
||||||
|
|
||||||
A quick rundown:
|
A quick rundown:
|
||||||
|
|
||||||
* The above template displays a radio button for each poll choice. The
|
* The above template displays a radio button for each question choice. The
|
||||||
``value`` of each radio button is the associated poll choice's ID. 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
|
``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
|
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.
|
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
|
set ``method="post"``. Using ``method="post"`` (as opposed to
|
||||||
``method="get"``) is very important, because the act of submitting this
|
``method="get"``) is very important, because the act of submitting this
|
||||||
form will alter data server-side. Whenever you create a form that alters
|
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
|
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::
|
||||||
|
|
||||||
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
|
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``::
|
||||||
|
@ -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.shortcuts import get_object_or_404, render
|
||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from polls.models import Choice, Poll
|
from polls.models import Choice, Question
|
||||||
# ...
|
# ...
|
||||||
def vote(request, poll_id):
|
def vote(request, question_id):
|
||||||
p = get_object_or_404(Poll, pk=poll_id)
|
p = get_object_or_404(Question, pk=question_id)
|
||||||
try:
|
try:
|
||||||
selected_choice = p.choice_set.get(pk=request.POST['choice'])
|
selected_choice = p.choice_set.get(pk=request.POST['choice'])
|
||||||
except (KeyError, Choice.DoesNotExist):
|
except (KeyError, Choice.DoesNotExist):
|
||||||
# Redisplay the poll voting form.
|
# Redisplay the question voting form.
|
||||||
return render(request, 'polls/detail.html', {
|
return render(request, 'polls/detail.html', {
|
||||||
'poll': p,
|
'question': p,
|
||||||
'error_message': "You didn't select a choice.",
|
'error_message': "You didn't select a choice.",
|
||||||
})
|
})
|
||||||
else:
|
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
|
* ``request.POST['choice']`` will raise :exc:`~exceptions.KeyError` if
|
||||||
``choice`` wasn't provided in POST data. The above code checks for
|
``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.
|
message if ``choice`` isn't given.
|
||||||
|
|
||||||
* After incrementing the choice count, the code returns an
|
* 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
|
object. For more on :class:`~django.http.HttpRequest` objects, see the
|
||||||
:doc:`request and response documentation </ref/request-response>`.
|
:doc:`request and response documentation </ref/request-response>`.
|
||||||
|
|
||||||
After somebody votes in a poll, the ``vote()`` view redirects to the results
|
After somebody votes in a question, the ``vote()`` view redirects to the results
|
||||||
page for the poll. Let's write that view::
|
page for the question. Let's write that view::
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
|
||||||
def results(request, poll_id):
|
|
||||||
poll = get_object_or_404(Poll, pk=poll_id)
|
def results(request, question_id):
|
||||||
return render(request, 'polls/results.html', {'poll': poll})
|
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
|
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
|
</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
|
.. code-block:: html+django
|
||||||
|
|
||||||
<h1>{{ poll.question }}</h1>
|
<h1>{{ question.question_text }}</h1>
|
||||||
|
|
||||||
<ul>
|
<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>
|
<li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</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
|
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.
|
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'^$', views.IndexView.as_view(), name='index'),
|
||||||
url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
|
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<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
|
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.core.urlresolvers import reverse
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
|
|
||||||
from polls.models import Choice, Poll
|
from polls.models import Choice, Question
|
||||||
|
|
||||||
|
|
||||||
class IndexView(generic.ListView):
|
class IndexView(generic.ListView):
|
||||||
template_name = 'polls/index.html'
|
template_name = 'polls/index.html'
|
||||||
context_object_name = 'latest_poll_list'
|
context_object_name = 'latest_question_list'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Return the last five published polls."""
|
"""Return the last five published questions."""
|
||||||
return Poll.objects.order_by('-pub_date')[:5]
|
return Question.objects.order_by('-pub_date')[:5]
|
||||||
|
|
||||||
|
|
||||||
class DetailView(generic.DetailView):
|
class DetailView(generic.DetailView):
|
||||||
model = Poll
|
model = Question
|
||||||
template_name = 'polls/detail.html'
|
template_name = 'polls/detail.html'
|
||||||
|
|
||||||
|
|
||||||
class ResultsView(generic.DetailView):
|
class ResultsView(generic.DetailView):
|
||||||
model = Poll
|
model = Question
|
||||||
template_name = 'polls/results.html'
|
template_name = 'polls/results.html'
|
||||||
|
|
||||||
def vote(request, poll_id):
|
|
||||||
|
def vote(request, question_id):
|
||||||
....
|
....
|
||||||
|
|
||||||
We're using two generic views here:
|
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
|
* The :class:`~django.views.generic.detail.DetailView` generic view
|
||||||
expects the primary key value captured from the URL to be called
|
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.
|
views.
|
||||||
|
|
||||||
By default, the :class:`~django.views.generic.detail.DetailView` generic
|
By default, the :class:`~django.views.generic.detail.DetailView` generic
|
||||||
view uses a template called ``<app name>/<model name>_detail.html``.
|
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`` attribute is used to tell Django to use a specific
|
||||||
template name instead of the autogenerated default template name. We
|
template name instead of the autogenerated default template name. We
|
||||||
also specify the ``template_name`` for the ``results`` list view --
|
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.
|
``"polls/index.html"`` template.
|
||||||
|
|
||||||
In previous parts of the tutorial, the templates have been provided
|
In previous parts of the tutorial, the templates have been provided
|
||||||
with a context that contains the ``poll`` and ``latest_poll_list``
|
with a context that contains the ``question`` and ``latest_question_list``
|
||||||
context variables. For ``DetailView`` the ``poll`` variable is provided
|
context variables. For ``DetailView`` the ``question`` variable is provided
|
||||||
automatically -- since we're using a Django model (``Poll``), Django
|
automatically -- since we're using a Django model (``Question``), Django
|
||||||
is able to determine an appropriate name for the context variable.
|
is able to determine an appropriate name for the context variable.
|
||||||
However, for ListView, the automatically generated context variable is
|
However, for ListView, the automatically generated context variable is
|
||||||
``poll_list``. To override this we provide the ``context_object_name``
|
``question_list``. To override this we provide the ``context_object_name``
|
||||||
attribute, specifying that we want to use ``latest_poll_list`` instead.
|
attribute, specifying that we want to use ``latest_question_list`` instead.
|
||||||
As an alternative approach, you could change your templates to match
|
As an alternative approach, you could change your templates to match
|
||||||
the new default context variables -- but it's a lot easier to just
|
the new default context variables -- but it's a lot easier to just
|
||||||
tell Django to use the variable you want.
|
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
|
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
|
right away: the ``Question.was_published_recently()`` method returns ``True`` if
|
||||||
the ``Poll`` was published within the last day (which is correct) but also if
|
the ``Question`` 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).
|
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 can see this in the Admin; create a question whose date lies in the future;
|
||||||
you'll see that the ``Poll`` change list claims it was published recently.
|
you'll see that the ``Question`` change list claims it was published recently.
|
||||||
|
|
||||||
You can also see this using the shell::
|
You can also see this using the shell::
|
||||||
|
|
||||||
>>> import datetime
|
>>> import datetime
|
||||||
>>> from django.utils import timezone
|
>>> from django.utils import timezone
|
||||||
>>> from polls.models import Poll
|
>>> from polls.models import Question
|
||||||
>>> # create a Poll instance with pub_date 30 days in the future
|
>>> # create a Question instance with pub_date 30 days in the future
|
||||||
>>> future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
|
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
|
||||||
>>> # was it published recently?
|
>>> # was it published recently?
|
||||||
>>> future_poll.was_published_recently()
|
>>> future_question.was_published_recently()
|
||||||
True
|
True
|
||||||
|
|
||||||
Since things in the future are not 'recent', this is clearly wrong.
|
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.utils import timezone
|
||||||
from django.test import TestCase
|
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
|
pub_date is in the future
|
||||||
"""
|
"""
|
||||||
future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
|
time = timezone.now() + datetime.timedelta(days=30)
|
||||||
self.assertEqual(future_poll.was_published_recently(), False)
|
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
|
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
|
future. We then check the output of ``was_published_recently()`` - which
|
||||||
*ought* to be False.
|
*ought* to be False.
|
||||||
|
|
||||||
|
@ -196,11 +197,11 @@ and you'll see something like::
|
||||||
Creating test database for alias 'default'...
|
Creating test database for alias 'default'...
|
||||||
F
|
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):
|
Traceback (most recent call last):
|
||||||
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll
|
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
|
||||||
self.assertEqual(future_poll.was_published_recently(), False)
|
self.assertEqual(future_question.was_published_recently(), False)
|
||||||
AssertionError: True != False
|
AssertionError: True != False
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
@ -219,7 +220,7 @@ What happened is this:
|
||||||
|
|
||||||
* it looked for test methods - ones whose names begin with ``test``
|
* 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
|
instance whose ``pub_date`` field is 30 days in the future
|
||||||
|
|
||||||
* ... and using the ``assertEqual()`` method, it discovered that its
|
* ... and using the ``assertEqual()`` method, it discovered that its
|
||||||
|
@ -232,7 +233,7 @@ occurred.
|
||||||
Fixing the bug
|
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
|
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
|
``models.py``, so that it will only return ``True`` if the date is also in the
|
||||||
past::
|
past::
|
||||||
|
@ -269,24 +270,26 @@ introduced another.
|
||||||
Add two more test methods to the same class, to test the behavior of the method
|
Add two more test methods to the same class, to test the behavior of the method
|
||||||
more comprehensively::
|
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
|
was_published_recently() should return False for questions whose
|
||||||
is older than 1 day
|
pub_date is older than 1 day
|
||||||
"""
|
"""
|
||||||
old_poll = Poll(pub_date=timezone.now() - datetime.timedelta(days=30))
|
time = timezone.now() - datetime.timedelta(days=30)
|
||||||
self.assertEqual(old_poll.was_published_recently(), False)
|
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
|
was_published_recently() should return True for questions whose
|
||||||
is within the last day
|
pub_date is within the last day
|
||||||
"""
|
"""
|
||||||
recent_poll = Poll(pub_date=timezone.now() - datetime.timedelta(hours=1))
|
time = timezone.now() - datetime.timedelta(hours=1)
|
||||||
self.assertEqual(recent_poll.was_published_recently(), True)
|
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()``
|
And now we have three tests that confirm that ``Question.was_published_recently()``
|
||||||
returns sensible values for past, recent, and future polls.
|
returns sensible values for past, recent, and future questions.
|
||||||
|
|
||||||
Again, ``polls`` is a simple application, but however complex it grows in the
|
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
|
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
|
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
|
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.
|
published at that moment, but invisible until then.
|
||||||
|
|
||||||
A test for a view
|
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
|
``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
|
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
|
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
|
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
|
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``
|
>>> # note - you might get unexpected results if your ``TIME_ZONE``
|
||||||
>>> # in ``settings.py`` is not correct. If you need to change it,
|
>>> # in ``settings.py`` is not correct. If you need to change it,
|
||||||
>>> # you will also need to restart your shell session
|
>>> # you will also need to restart your shell session
|
||||||
>>> from polls.models import Poll
|
>>> from polls.models import Question
|
||||||
>>> from django.utils import timezone
|
>>> from django.utils import timezone
|
||||||
>>> # create a Poll and save it
|
>>> # create a Question and save it
|
||||||
>>> p = Poll(question="Who is your favorite Beatle?", pub_date=timezone.now())
|
>>> q = Question(question_text="Who is your favorite Beatle?", pub_date=timezone.now())
|
||||||
>>> p.save()
|
>>> q.save()
|
||||||
>>> # check the response once again
|
>>> # check the response once again
|
||||||
>>> response = client.get('/polls/')
|
>>> response = client.get('/polls/')
|
||||||
>>> response.content
|
>>> 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'
|
'\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']
|
>>> response.context['latest_question_list']
|
||||||
[<Poll: Who is your favorite Beatle?>]
|
[<Question: Who is your favorite Beatle?>]
|
||||||
|
|
||||||
Improving our view
|
Improving our view
|
||||||
------------------
|
------------------
|
||||||
|
@ -383,13 +386,13 @@ based on :class:`~django.views.generic.list.ListView`::
|
||||||
|
|
||||||
class IndexView(generic.ListView):
|
class IndexView(generic.ListView):
|
||||||
template_name = 'polls/index.html'
|
template_name = 'polls/index.html'
|
||||||
context_object_name = 'latest_poll_list'
|
context_object_name = 'latest_question_list'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Return the last five published polls."""
|
"""Return the last five published questions."""
|
||||||
return Poll.objects.order_by('-pub_date')[:5]
|
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.
|
places into the context.
|
||||||
|
|
||||||
We need to amend the ``get_queryset`` method and change it so that it also
|
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):
|
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).
|
published in the future).
|
||||||
"""
|
"""
|
||||||
return Poll.objects.filter(
|
return Question.objects.filter(
|
||||||
pub_date__lte=timezone.now()
|
pub_date__lte=timezone.now()
|
||||||
).order_by('-pub_date')[:5]
|
).order_by('-pub_date')[:5]
|
||||||
|
|
||||||
``Poll.objects.filter(pub_date__lte=timezone.now())`` returns a queryset
|
``Question.objects.filter(pub_date__lte=timezone.now())`` returns a queryset
|
||||||
containing Polls whose ``pub_date`` is less than or equal to - that is, earlier
|
containing ``Question``\s whose ``pub_date`` is less than or equal to - that
|
||||||
than or equal to - ``timezone.now``.
|
is, earlier than or equal to - ``timezone.now``.
|
||||||
|
|
||||||
Testing our new view
|
Testing our new view
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
Now you can satisfy yourself that this behaves as expected by firing up the
|
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
|
runserver, loading the site in your browser, creating ``Questions`` with dates
|
||||||
the past and future, and checking that only those that have been published are
|
in the past and future, and checking that only those that have been published
|
||||||
listed. You don't want to have to do that *every single time you make any
|
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
|
change that might affect this* - so let's also create a test, based on our
|
||||||
shell session above.
|
shell session above.
|
||||||
|
|
||||||
|
@ -427,91 +430,98 @@ Add the following to ``polls/tests.py``::
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
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
|
Creates a question with the given `question_text` published the given
|
||||||
`days` offset to now (negative for polls published in the past,
|
number of `days` offset to now (negative for questions published
|
||||||
positive for polls that have yet to be published).
|
in the past, positive for questions that have yet to be published).
|
||||||
"""
|
"""
|
||||||
return Poll.objects.create(question=question,
|
time = timezone.now() + datetime.timedelta(days=days)
|
||||||
pub_date=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'))
|
response = self.client.get(reverse('polls:index'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "No polls are available.")
|
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'))
|
response = self.client.get(reverse('polls:index'))
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
response.context['latest_poll_list'],
|
response.context['latest_question_list'],
|
||||||
['<Poll: Past poll.>']
|
['<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
|
Questions with a pub_date in the future should not be displayed on
|
||||||
index page.
|
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'))
|
response = self.client.get(reverse('polls:index'))
|
||||||
self.assertContains(response, "No polls are available.", status_code=200)
|
self.assertContains(response, "No polls are available.",
|
||||||
self.assertQuerysetEqual(response.context['latest_poll_list'], [])
|
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
|
Even if both past and future questions exist, only past questions
|
||||||
displayed.
|
should be displayed.
|
||||||
"""
|
"""
|
||||||
create_poll(question="Past poll.", days=-30)
|
create_question(question_text="Past question.", days=-30)
|
||||||
create_poll(question="Future poll.", days=30)
|
create_question(question_text="Future question.", days=30)
|
||||||
response = self.client.get(reverse('polls:index'))
|
response = self.client.get(reverse('polls:index'))
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
response.context['latest_poll_list'],
|
response.context['latest_question_list'],
|
||||||
['<Poll: Past poll.>']
|
['<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_question(question_text="Past quesiton 1.", days=-30)
|
||||||
create_poll(question="Past poll 2.", days=-5)
|
create_question(question_text="Past question 2.", days=-5)
|
||||||
response = self.client.get(reverse('polls:index'))
|
response = self.client.get(reverse('polls:index'))
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
response.context['latest_poll_list'],
|
response.context['latest_question_list'],
|
||||||
['<Poll: Past poll 2.>', '<Poll: Past poll 1.>']
|
['<Question: Past question 2.>', '<Question: Past question 1.>']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
Let's look at some of these more closely.
|
Let's look at some of these more closely.
|
||||||
|
|
||||||
First is a poll factory method, ``create_poll``, to take some repetition out
|
First is a question factory method, ``create_question``, to take some
|
||||||
of the process of creating polls.
|
repetition out of the process of creating questions.
|
||||||
|
|
||||||
``test_index_view_with_no_polls`` doesn't create any polls, but checks the
|
``test_index_view_with_no_questions`` doesn't create any questions, but checks
|
||||||
message: "No polls are available." and verifies the ``latest_poll_list`` is
|
the message: "No polls are available." and verifies the ``latest_question_list``
|
||||||
empty. Note that the :class:`django.test.TestCase` class provides some
|
is empty. Note that the :class:`django.test.TestCase` class provides some
|
||||||
additional assertion methods. In these examples, we use
|
additional assertion methods. In these examples, we use
|
||||||
:meth:`~django.test.SimpleTestCase.assertContains()` and
|
:meth:`~django.test.SimpleTestCase.assertContains()` and
|
||||||
:meth:`~django.test.TransactionTestCase.assertQuerysetEqual()`.
|
: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.
|
appears in the list.
|
||||||
|
|
||||||
In ``test_index_view_with_a_future_poll``, we create a poll with a ``pub_date``
|
In ``test_index_view_with_a_future_question``, we create a question with a
|
||||||
in the future. The database is reset for each test method, so the first poll is
|
``pub_date`` in the future. The database is reset for each test method, so the
|
||||||
no longer there, and so again the index shouldn't have any polls in it.
|
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 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
|
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``
|
Testing the ``DetailView``
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
What we have works well; however, even though future polls don't appear in the
|
What we have works well; however, even though future questions don't appear in
|
||||||
*index*, users can still reach them if they know or guess the right URL. So we
|
the *index*, users can still reach them if they know or guess the right URL. So
|
||||||
need to add a similar constraint to ``DetailView``::
|
we need to add a similar constraint to ``DetailView``::
|
||||||
|
|
||||||
|
|
||||||
class DetailView(generic.DetailView):
|
class DetailView(generic.DetailView):
|
||||||
...
|
...
|
||||||
def get_queryset(self):
|
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``
|
``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::
|
||||||
|
|
||||||
class PollIndexDetailTests(TestCase):
|
class QuestionIndexDetailTests(TestCase):
|
||||||
def test_detail_view_with_a_future_poll(self):
|
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.
|
return a 404 not found.
|
||||||
"""
|
"""
|
||||||
future_poll = create_poll(question='Future poll.', days=5)
|
future_question = create_question(question_text='Future question.',
|
||||||
response = self.client.get(reverse('polls:detail', args=(future_poll.id,)))
|
days=5)
|
||||||
|
response = self.client.get(reverse('polls:detail',
|
||||||
|
args=(future_question.id,)))
|
||||||
self.assertEqual(response.status_code, 404)
|
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 detail view of a question with a pub_date in the past should
|
||||||
the poll's question.
|
display the question's text.
|
||||||
"""
|
"""
|
||||||
past_poll = create_poll(question='Past Poll.', days=-5)
|
past_question = create_question(question_text='Past Question.',
|
||||||
response = self.client.get(reverse('polls:detail', args=(past_poll.id,)))
|
days=-5)
|
||||||
self.assertContains(response, past_poll.question, status_code=200)
|
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
|
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.
|
just created; in fact there will be a lot of repetition.
|
||||||
|
|
||||||
We could also improve our application in other ways, adding tests along the
|
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
|
way. For example, it's silly that ``Questions`` can be published on the site
|
||||||
have no ``Choices``. So, our views could check for this, and exclude such
|
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
|
``Questions``. Our tests would create a ``Question`` without ``Choices`` and
|
||||||
that it's not published, as well as create a similar ``Poll`` *with*
|
then test that it's not published, as well as create a similar ``Question``
|
||||||
``Choices``, and test that it *is* published.
|
*with* ``Choices``, and test that it *is* published.
|
||||||
|
|
||||||
Perhaps logged-in admin users should be allowed to see unpublished ``Polls``,
|
Perhaps logged-in admin users should be allowed to see unpublished
|
||||||
but not ordinary visitors. Again: whatever needs to be added to the software to
|
``Questions``, but not ordinary visitors. Again: whatever needs to be added to
|
||||||
accomplish this should be accompanied by a test, whether you write the test
|
the software to accomplish this should be accompanied by a test, whether you
|
||||||
first and then make the code pass the test, or work out the logic in your code
|
write the test first and then make the code pass the test, or work out the
|
||||||
first and then write a test to prove it.
|
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
|
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:
|
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.
|
as you continue to develop your program.
|
||||||
|
|
||||||
Sometimes tests will need to be updated. Suppose that we amend our views so that
|
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
|
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.
|
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.
|
template tag generates the absolute URL of the static file.
|
||||||
|
|
||||||
That's all you need to do for development. Reload
|
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.
|
green (Django style!) which means that your stylesheet was properly loaded.
|
||||||
|
|
||||||
Adding a background-image
|
Adding a background-image
|
||||||
|
|