2005-07-17 02:54:15 +02:00
|
|
|
=====================================
|
|
|
|
Writing your first Django app, part 2
|
|
|
|
=====================================
|
|
|
|
|
|
|
|
By Adrian Holovaty <holovaty@gmail.com>
|
|
|
|
|
|
|
|
This tutorial begins where `Tutorial 1`_ left off. We're continuing the Web-poll
|
|
|
|
application and will focus on Django's automatically-generated admin site.
|
|
|
|
|
|
|
|
.. _Tutorial 1: http://www.djangoproject.com/documentation/tutorial1/
|
|
|
|
|
|
|
|
Philosophy
|
|
|
|
==========
|
|
|
|
|
|
|
|
Generating admin sites for your staff or clients to add, change and delete
|
|
|
|
content is tedious work that doesn't require much creativity. For that reason,
|
|
|
|
Django entirely automates creation of admin interfaces for models.
|
|
|
|
|
|
|
|
Django was written in a newsroom environment, with a very clear separation
|
|
|
|
between "content publishers" and the "public" site. Site managers use the
|
|
|
|
system to add news stories, events, sports scores, etc., and that content is
|
|
|
|
displayed on the public site. Django solves the problem of creating a unified
|
|
|
|
interface for site administrators to edit content.
|
|
|
|
|
|
|
|
The admin isn't necessarily intended to be used by site visitors; it's for site
|
|
|
|
managers.
|
|
|
|
|
|
|
|
Expose the admin media files
|
|
|
|
============================
|
|
|
|
|
|
|
|
Django's admin is intended to be fully functional and good looking. For that
|
|
|
|
reason, Django ships with admin media files -- CSS, JavaScript and images --
|
|
|
|
that comprise the admin's design. To set up a Django admin instance, the first
|
|
|
|
thing to do is put those media files online somewhere.
|
|
|
|
|
|
|
|
(Note: Although Django ships with a default design for its admin site, you can
|
|
|
|
change it however you'd like. The admin uses Django's own template system and
|
|
|
|
is powered -- surprise, surprise -- by Django itself, so it is completely
|
|
|
|
customizable.)
|
|
|
|
|
|
|
|
The files are in the ``media`` directory of the Django distribution. To
|
|
|
|
"activate" them, copy that directory under a Web document root somewhere, so
|
|
|
|
that you can access them via the Web.
|
|
|
|
|
|
|
|
Be careful not to put your Python source code under the Web document root. Just
|
|
|
|
do the media files.
|
|
|
|
|
|
|
|
Then, tell Django where you put them, via ``ADMIN_MEDIA_PREFIX`` in the
|
|
|
|
``myproject.settings.admin`` settings file. Examples::
|
|
|
|
|
|
|
|
# You can leave off the domain if they're on the same domain as your admin
|
|
|
|
# site will be.
|
|
|
|
ADMIN_MEDIA_PREFIX = '/adminmedia/'
|
|
|
|
|
|
|
|
# Otherwise, use the fully-qualified domain.
|
|
|
|
ADMIN_MEDIA_PREFIX = 'http://www.foo.com/adminmedia/'
|
|
|
|
|
|
|
|
Make sure to include a trailing slash.
|
|
|
|
|
|
|
|
Hook into mod_python
|
|
|
|
====================
|
|
|
|
|
|
|
|
Now let's take the opportunity to hook Django into Apache/mod_python. Edit your
|
|
|
|
``httpd.conf`` file and add this::
|
|
|
|
|
|
|
|
<Location "/admin/">
|
|
|
|
SetHandler python-program
|
|
|
|
PythonHandler django.core.handler
|
|
|
|
SetEnv DJANGO_SETTINGS_MODULE myproject.settings.admin
|
|
|
|
</Location>
|
|
|
|
|
|
|
|
This tells Apache: "Use mod_python for any URL at or under '/admin/', using the
|
|
|
|
Django mod_python handler." It also passes the value of ``DJANGO_SETTINGS_MODULE``,
|
|
|
|
so mod_python knows which project to use. Note that we're passing the path to
|
|
|
|
the ``admin`` settings, not the ``main`` settings. That's because this is the
|
|
|
|
admin site, which has slightly different settings.
|
|
|
|
|
|
|
|
You can also add directives such as ``PythonAutoReload Off`` for performance.
|
|
|
|
See the `mod_python documentation`_ for a full list of options.
|
|
|
|
|
|
|
|
When you've done that, restart Apache and go to /admin/ on your domain. You
|
|
|
|
should see the admin's login screen:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin01.png
|
|
|
|
:alt: Django admin login screen
|
|
|
|
|
|
|
|
.. _mod_python documentation: http://modpython.org/live/current/doc-html/directives.html
|
|
|
|
|
|
|
|
Create a user account
|
|
|
|
=====================
|
|
|
|
|
|
|
|
We can't log in, though, because we haven't created an admin user account yet.
|
|
|
|
Drop into the Python interactive interpreter and type this::
|
|
|
|
|
|
|
|
# The function django.models.auth.users.create_user() creates a new user
|
|
|
|
# and returns the new auth.User object.
|
|
|
|
# Don't use 'username' and 'password'. Those are just examples.
|
|
|
|
>>> from django.models.auth import users
|
|
|
|
>>> u = users.create_user('username', 'your_email@domain.com', 'password')
|
|
|
|
|
|
|
|
# But we're not done. We need to explicitly set is_staff and is_active to
|
|
|
|
# allow this user to access the admin. Might as well make it a superuser,
|
|
|
|
# too.
|
|
|
|
u.is_staff = True
|
|
|
|
u.is_active = True
|
|
|
|
u.is_superuser = True
|
|
|
|
|
|
|
|
# Remember, call the save() method to save changes.
|
|
|
|
u.save()
|
|
|
|
|
|
|
|
Enter the admin site
|
|
|
|
====================
|
|
|
|
|
2005-07-17 04:59:31 +02:00
|
|
|
Now, try logging in.
|
|
|
|
|
|
|
|
If it didn't work, read the `"I can't log in" questions`_ in the FAQ.
|
|
|
|
|
|
|
|
If it worked, you should see the Django admin index page:
|
2005-07-17 02:54:15 +02:00
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin02t.png
|
|
|
|
:alt: Django admin index page
|
|
|
|
:target: http://media.djangoproject.com/img/doc/tutorial/admin02.png
|
|
|
|
|
|
|
|
By default, you should see four types of editable content: groups, users,
|
|
|
|
redirects and flat files. These are core features Django ships with by default.
|
|
|
|
|
2005-07-17 04:59:31 +02:00
|
|
|
.. _"I can't log in" questions: http://www.djangoproject.com/documentation/faq/#the-admin-site
|
|
|
|
|
2005-07-17 02:54:15 +02:00
|
|
|
Make the poll app modifiable in the admin
|
|
|
|
=========================================
|
|
|
|
|
|
|
|
But where's our poll app? It's not displayed on the admin index page.
|
|
|
|
|
|
|
|
Just one thing to do: We need to specify in the ``polls.Poll`` model that Poll
|
|
|
|
objects have an admin interface. Edit the ``myproject/apps/polls/models/polls.py``
|
|
|
|
file and make the following change to add an ``admin`` attribute::
|
|
|
|
|
|
|
|
class Poll(meta.Model):
|
|
|
|
fields = (
|
|
|
|
# ...
|
|
|
|
)
|
|
|
|
admin = meta.Admin(
|
|
|
|
fields = (
|
|
|
|
(None, {'fields': ('question', 'pub_date')}),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
Reload the Django admin index page. Note that you might have to restart Apache,
|
|
|
|
depending on your Apache settings. Because mod_python saves code in memory for
|
|
|
|
performance, Python code changes generally aren't reflected until Apache
|
|
|
|
restarts. One way around this is to set ``MaxRequestsPerChild 1`` in your
|
|
|
|
httpd.conf to force Apache to reload everything for each request. But don't do
|
|
|
|
that on a production server, or we'll revoke your Django privileges.
|
|
|
|
|
|
|
|
Explore the free admin functionality
|
|
|
|
====================================
|
|
|
|
|
|
|
|
Now that ``Poll`` has the ``admin`` attribute, Django knows that it should be
|
|
|
|
displayed on the admin index page:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin03t.png
|
|
|
|
:alt: Django admin index page, now with polls displayed
|
|
|
|
:target: http://media.djangoproject.com/img/doc/tutorial/admin03.png
|
|
|
|
|
|
|
|
Click "Polls." Now you're at the "change list" page for polls. This page
|
|
|
|
displays all the polls in the database and lets you choose one to change it.
|
|
|
|
There's the "What's up?" poll we created in the first tutorial:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin04t.png
|
|
|
|
:alt: Polls change list page
|
|
|
|
:target: http://media.djangoproject.com/img/doc/tutorial/admin04.png
|
|
|
|
|
|
|
|
Click the "What's up?" poll to edit it:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin05t.png
|
|
|
|
:alt: Editing form for poll object
|
|
|
|
:target: http://media.djangoproject.com/img/doc/tutorial/admin05.png
|
|
|
|
|
|
|
|
Things to note here:
|
|
|
|
|
|
|
|
* The form is automatically generated from the Poll model.
|
|
|
|
* The different model field types (``meta.DateTimeField``, ``meta.CharField``)
|
|
|
|
correspond to the appropriate HTML input widget. Each type of field knows
|
|
|
|
how to display itself in the Django admin.
|
2005-07-17 03:00:25 +02:00
|
|
|
* Each ``DateTimeField`` gets free JavaScript shortcuts. Dates get a "Today"
|
2005-07-17 02:54:15 +02:00
|
|
|
shortcut and calendar popup, and times get a "Now" shortcut and a convenient
|
|
|
|
popup that lists commonly entered times.
|
2005-07-17 03:00:25 +02:00
|
|
|
|
|
|
|
The bottom part of the page gives you a couple of options:
|
|
|
|
|
|
|
|
* Save -- Saves changes and returns to the change-list page for this type of
|
|
|
|
object.
|
|
|
|
* Save and continue editing -- Saves changes and reloads the admin page for
|
|
|
|
this object.
|
|
|
|
* Save and add another -- Saves changes and loads a new, blank form for this
|
|
|
|
type of object.
|
|
|
|
* Delete -- Displays a delete confirmation page.
|
2005-07-17 02:54:15 +02:00
|
|
|
|
|
|
|
Change the "Date published" by clicking the "Today" and "Now" shortcuts. Then
|
|
|
|
click "Save and continue editing." Then click "History" in the upper right.
|
|
|
|
You'll see a page listing all changes made to this object via the Django admin,
|
|
|
|
with the timestamp and username of the person who made the change:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin06t.png
|
|
|
|
:alt: History page for poll object
|
|
|
|
:target: http://media.djangoproject.com/img/doc/tutorial/admin06.png
|
|
|
|
|
|
|
|
Customize the admin form
|
|
|
|
========================
|
|
|
|
|
|
|
|
Take a few minutes to marvel at all the code you didn't have to write.
|
|
|
|
|
|
|
|
Let's customize this a bit. We can reorder the fields by changing the
|
|
|
|
order of the field names in the ``admin`` attribute of the model::
|
|
|
|
|
|
|
|
admin = meta.Admin(
|
|
|
|
fields = (
|
|
|
|
(None, {'fields': ('pub_date', 'question')}),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
That made the "Publication date" show up first instead of second:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin07.png
|
|
|
|
:alt: Fields have been reordered
|
|
|
|
|
|
|
|
This isn't impressive with only two fields, but for admin forms with dozens
|
|
|
|
of fields, choosing an intuitive order is an important usability detail.
|
|
|
|
|
|
|
|
And speaking of forms with dozens of fields, you might want to split the form
|
|
|
|
up into fieldsets::
|
|
|
|
|
|
|
|
admin = meta.Admin(
|
|
|
|
fields = (
|
|
|
|
(None, {'fields': ('question',)}),
|
|
|
|
('Date information', {'fields': ('pub_date',)}),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
The first element of each tuple in ``fields`` is the title of the fieldset.
|
|
|
|
Here's what our form looks like now:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin08t.png
|
|
|
|
:alt: Form has fieldsets now
|
|
|
|
:target: http://media.djangoproject.com/img/doc/tutorial/admin08.png
|
|
|
|
|
|
|
|
You can assign arbitrary HTML classes to each fieldset. Django provides a
|
|
|
|
``"collapse"`` class that displays a particular fieldset initially collapsed.
|
|
|
|
This is useful when you have a long form that contains a number of fields that
|
|
|
|
aren't commonly used::
|
|
|
|
|
|
|
|
admin = meta.Admin(
|
|
|
|
fields = (
|
|
|
|
(None, {'fields': ('question',)}),
|
|
|
|
('Date information', {'fields': ('pub_date',), 'classes': 'collapse'}),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin09.png
|
|
|
|
:alt: Fieldset is initially collapsed
|
|
|
|
|
2005-07-17 07:21:19 +02:00
|
|
|
Adding related objects
|
|
|
|
======================
|
|
|
|
|
|
|
|
OK, we have our Poll admin page. But a ``Poll`` has multiple ``Choices``, and the admin
|
|
|
|
page doesn't display choices.
|
|
|
|
|
|
|
|
Yet.
|
|
|
|
|
|
|
|
In this case, there are two ways to solve this problem. The first is to give
|
|
|
|
the ``Choice`` model its own ``admin`` attribute, just as we did with ``Poll``.
|
|
|
|
Here's what that would look like::
|
|
|
|
|
|
|
|
class Choice(meta.Model):
|
|
|
|
# ...
|
|
|
|
admin = meta.Admin(
|
|
|
|
fields = (
|
|
|
|
(None, {'fields': ('poll_id', 'choice', 'votes')}),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
(Note that we used "poll_id" to refer to the ``ForeignKey(Poll)`` field. The
|
|
|
|
field name is automatically calculated from the model's class name, lowercased,
|
|
|
|
plus '_id'.)
|
|
|
|
|
|
|
|
Now "Choices" is an available option in the Django admin. The "Add choice" form
|
2005-07-17 07:22:48 +02:00
|
|
|
looks like this:
|
2005-07-17 07:21:19 +02:00
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin10.png
|
|
|
|
:alt: Choice admin page
|
|
|
|
|
|
|
|
In that form, the "Poll" field is a select box containing every poll in the
|
|
|
|
database. In our case, only one poll exists at this point.
|
|
|
|
|
|
|
|
Also note the "Add Another" link next to "Poll." Every object with a ForeignKey
|
|
|
|
relationship to another gets this for free. When you click "Add Another," you'll
|
|
|
|
get a popup window with the "Add poll" form. If you add a poll in that window
|
|
|
|
and click "Save," Django will save the poll to the database and dynamically add
|
|
|
|
it as the selected choice on the "Add choice" form you're looking at.
|
|
|
|
|
|
|
|
But, really, this is an inefficient way of adding Choice objects to the system.
|
|
|
|
It'd be better if you could add a bunch of Choices directly when you create the
|
|
|
|
Poll object. Let's make that happen.
|
|
|
|
|
|
|
|
Remove the ``admin`` for the Choice model. Then, edit the ``ForeignKey(Poll)``
|
|
|
|
field like so::
|
|
|
|
|
|
|
|
meta.ForeignKey(Poll, edit_inline=True, num_in_admin=3),
|
|
|
|
|
|
|
|
This tells Django: "Choice objects are edited on the Poll admin page. By
|
|
|
|
default, provide enough fields for 3 Choices."
|
|
|
|
|
|
|
|
Then change the other fields in ``Choice`` to give them ``core=True``::
|
|
|
|
|
|
|
|
meta.CharField('choice', 'choice', maxlength=200, core=True),
|
|
|
|
meta.IntegerField('votes', 'votes', core=True),
|
|
|
|
|
|
|
|
This tells Django: "When you edit a Choice on the Poll admin page, the 'choice'
|
|
|
|
and 'votes' fields are required. The presence of at least one of them signifies
|
|
|
|
the addition of a new Choice object, and clearing at least one of them
|
|
|
|
signifies the deletion of that existing Choice object."
|
|
|
|
|
|
|
|
Load the "Add poll" page to see how that looks:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin11t.png
|
|
|
|
:alt: Add poll page now has choices on it
|
|
|
|
:target: http://media.djangoproject.com/img/doc/tutorial/admin11.png
|
|
|
|
|
|
|
|
It works like this: There are three slots for related Choices -- as specified
|
|
|
|
by ``num_in_admin`` -- but each time you come back to the "Change" page for an
|
|
|
|
already-created object, you get one extra slot. (This means there's no
|
|
|
|
hard-coded limit on how many related objects can be added.) If you wanted space
|
|
|
|
for three extra Choices each time you changed the poll, you'd use
|
|
|
|
``num_extra_on_change=3``.
|
|
|
|
|
|
|
|
One small problem, though. It takes a lot of screen space to display all the
|
|
|
|
fields for entering related Choice objects. For that reason, Django offers an
|
|
|
|
alternate way of displaying inline related objects::
|
|
|
|
|
|
|
|
meta.ForeignKey(Poll, edit_inline=True, num_in_admin=3, edit_inline_type=meta.TABULAR),
|
|
|
|
|
|
|
|
With that ``edit_inline_type=meta.TABULAR``, the related objects are displayed
|
|
|
|
in a more compact, table-based format:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin12.png
|
|
|
|
:alt: Add poll page now has more compact choices
|
|
|
|
|
2005-07-17 07:52:07 +02:00
|
|
|
Customize the admin change list
|
|
|
|
===============================
|
|
|
|
|
|
|
|
Now that the Poll admin page is looking good, let's make some tweaks to the
|
|
|
|
"change list" page -- the one that displays all the polls in the system.
|
|
|
|
|
|
|
|
Here's what it looks like at this point:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin04t.png
|
|
|
|
:alt: Polls change list page
|
|
|
|
:target: http://media.djangoproject.com/img/doc/tutorial/admin04.png
|
|
|
|
|
|
|
|
By default, Django displays the ``repr()`` of each object. But it'd be more
|
|
|
|
helpful if we could display individual fields. To do that, use the
|
|
|
|
``list_display`` option, which is a tuple of field names to display, as columns,
|
|
|
|
on the change list page for the object::
|
|
|
|
|
|
|
|
class Poll(meta.Model):
|
|
|
|
# ...
|
|
|
|
admin = meta.Admin(
|
|
|
|
fields = (
|
|
|
|
(None, {'fields': ('question', 'pub_date')}),
|
|
|
|
),
|
|
|
|
list_display = ('question', 'pub_date'),
|
|
|
|
)
|
|
|
|
|
|
|
|
Just for good measure, let's also include the ``was_published_today`` custom
|
|
|
|
method from Tutorial 1::
|
|
|
|
|
|
|
|
list_display = ('question', 'pub_date', 'was_published_today'),
|
|
|
|
|
|
|
|
Now the poll change list page looks like this:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin13t.png
|
|
|
|
:alt: Polls change list page, updated
|
|
|
|
:target: http://media.djangoproject.com/img/doc/tutorial/admin13.png
|
|
|
|
|
|
|
|
You can click on the column headers to sort by those values -- except in the
|
|
|
|
case of the ``was_published_today`` header, because sorting by the output of
|
|
|
|
an arbitrary method is not supported. Also note that the column header for
|
|
|
|
``was_published_today`` is, by default, the name of the method. But you can
|
|
|
|
change that by giving that method a ``short_description`` attribute::
|
|
|
|
|
|
|
|
def was_published_today(self):
|
|
|
|
return self.pub_date.date() == datetime.date.today()
|
|
|
|
was_published_today.short_description = 'Was published today'
|
|
|
|
|
|
|
|
|
|
|
|
Let's add another improvement to the Poll change list page: Filters. Add the
|
2005-07-17 07:53:34 +02:00
|
|
|
following line to ``Poll.admin``::
|
2005-07-17 07:52:07 +02:00
|
|
|
|
|
|
|
list_filter = ('pub_date', )
|
|
|
|
|
|
|
|
That adds a "Filter" sidebar that lets people filter the change list by the
|
|
|
|
``pub_date`` field:
|
|
|
|
|
|
|
|
.. image:: http://media.djangoproject.com/img/doc/tutorial/admin14t.png
|
|
|
|
:alt: Polls change list page, updated
|
|
|
|
:target: http://media.djangoproject.com/img/doc/tutorial/admin14.png
|
|
|
|
|
|
|
|
The type of filter displayed depends on the type of field you're filtering on.
|
|
|
|
Because ``pub_date`` is a DateTimeField, Django knows to give the default
|
|
|
|
filter options for DateTimeFields: "Any date," "Today," "Past 7 days,"
|
|
|
|
"This month," "This year." Explore using ``list_filter`` on other types of
|
|
|
|
fields.
|
|
|
|
|
2005-07-17 07:54:43 +02:00
|
|
|
This is shaping up well. Let's add some search capability::
|
2005-07-17 07:52:07 +02:00
|
|
|
|
|
|
|
search_fields = ('question', )
|
|
|
|
|
|
|
|
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
|
|
|
|
fields as you'd like -- although because it uses a LIKE query behind the
|
2005-07-17 07:54:43 +02:00
|
|
|
scenes, keep it reasonable, to keep your database happy.
|
2005-07-17 07:52:07 +02:00
|
|
|
|
|
|
|
Finally, because Poll objects have dates, it'd be convenient to be able to
|
|
|
|
drill down by date. Add this line::
|
|
|
|
|
|
|
|
date_hierarchy = 'pub_date',
|
|
|
|
|
|
|
|
That adds hierarchical navigation, by date, to the top of the change list page.
|
|
|
|
At top level, it displays all available years. Then it drills down to months
|
|
|
|
and, ultimately, days.
|
|
|
|
|
|
|
|
Now's also a good time to note that change lists give you free pagination. The
|
|
|
|
default is to display 50 items per page. Change-list pagination, search boxes,
|
|
|
|
filters, date-hierarchies and column-header-ordering all work together like you
|
|
|
|
think they should.
|
|
|
|
|
2005-07-17 07:21:19 +02:00
|
|
|
More
|
|
|
|
====
|
2005-07-17 02:54:15 +02:00
|
|
|
|
|
|
|
There's much more to come. This document is not finished.
|