123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- =====================================
- Writing your first Django app, part 7
- =====================================
- This tutorial begins where :doc:`Tutorial 6 </intro/tutorial06>` left off. We're
- continuing the web-poll application and will focus on customizing Django's
- automatically-generated admin site that we first explored in :doc:`Tutorial 2
- </intro/tutorial02>`.
- .. admonition:: Where to get help:
- If you're having trouble going through this tutorial, please head over to
- the :doc:`Getting Help</faq/help>` section of the FAQ.
- Customize the admin form
- ========================
- By registering the ``Question`` model with ``admin.site.register(Question)``,
- Django was able to construct a default form representation. Often, you'll want
- to customize how the admin form looks and works. You'll do this by telling
- Django the options you want when you register the object.
- Let's see how this works by reordering the fields on the edit form. Replace
- the ``admin.site.register(Question)`` line with:
- .. code-block:: python
- :caption: ``polls/admin.py``
- from django.contrib import admin
- from .models import Question
- class QuestionAdmin(admin.ModelAdmin):
- fields = ["pub_date", "question_text"]
- admin.site.register(Question, QuestionAdmin)
- You'll follow this pattern -- create a model admin class, then pass it as the
- second argument to ``admin.site.register()`` -- any time you need to change the
- admin options for a model.
- This particular change above makes the "Publication date" come before the
- "Question" field:
- .. image:: _images/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:
- .. code-block:: python
- :caption: ``polls/admin.py``
- from django.contrib import admin
- from .models import Question
- class QuestionAdmin(admin.ModelAdmin):
- fieldsets = [
- (None, {"fields": ["question_text"]}),
- ("Date information", {"fields": ["pub_date"]}),
- ]
- admin.site.register(Question, QuestionAdmin)
- The first element of each tuple in
- :attr:`~django.contrib.admin.ModelAdmin.fieldsets` is the title of the fieldset.
- Here's what our form looks like now:
- .. image:: _images/admin08t.png
- :alt: Form has fieldsets now
- Adding related objects
- ======================
- OK, we have our Question admin page, but a ``Question`` has multiple
- ``Choice``\s, and the admin page doesn't display choices.
- Yet.
- There are two ways to solve this problem. The first is to register ``Choice``
- with the admin just as we did with ``Question``:
- .. code-block:: python
- :caption: ``polls/admin.py``
- from django.contrib import admin
- from .models import Choice, Question
- # ...
- admin.site.register(Choice)
- Now "Choices" is an available option in the Django admin. The "Add choice" form
- looks like this:
- .. image:: _images/admin09.png
- :alt: Choice admin page
- In that form, the "Question" field is a select box containing every question in the
- database. Django knows that a :class:`~django.db.models.ForeignKey` should be
- represented in the admin as a ``<select>`` box. In our case, only one question
- exists at this point.
- Also note the "Add another question" link next to "Question." Every object with
- a ``ForeignKey`` relationship to another gets this for free. When you click
- "Add another question", you'll get a popup window with the "Add question" form.
- If you add a question in that window and click "Save", Django will save the
- question to the database and dynamically add it as the selected choice on the
- "Add choice" form you're looking at.
- But, really, this is an inefficient way of adding ``Choice`` objects to the system.
- It'd be better if you could add a bunch of Choices directly when you create the
- ``Question`` object. Let's make that happen.
- Remove the ``register()`` call for the ``Choice`` model. Then, edit the ``Question``
- registration code to read:
- .. code-block:: python
- :caption: ``polls/admin.py``
- from django.contrib import admin
- from .models import Choice, Question
- class ChoiceInline(admin.StackedInline):
- model = Choice
- extra = 3
- class QuestionAdmin(admin.ModelAdmin):
- fieldsets = [
- (None, {"fields": ["question_text"]}),
- ("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}),
- ]
- inlines = [ChoiceInline]
- admin.site.register(Question, QuestionAdmin)
- This tells Django: "``Choice`` objects are edited on the ``Question`` admin page. By
- default, provide enough fields for 3 choices."
- Load the "Add question" page to see how that looks:
- .. image:: _images/admin10t.png
- :alt: Add question page now has choices on it
- It works like this: There are three slots for related Choices -- as specified
- by ``extra`` -- and each time you come back to the "Change" page for an
- already-created object, you get another three extra slots.
- At the end of the three current slots you will find an "Add another Choice"
- link. If you click on it, a new slot will be added. If you want to remove the
- added slot, you can click on the X to the top right of the added slot. This
- image shows an added slot:
- .. image:: _images/admin14t.png
- :alt: Additional slot added dynamically
- One small problem, though. It takes a lot of screen space to display all the
- fields for entering related ``Choice`` objects. For that reason, Django offers a
- tabular way of displaying inline related objects. To use it, change the
- ``ChoiceInline`` declaration to read:
- .. code-block:: python
- :caption: ``polls/admin.py``
- class ChoiceInline(admin.TabularInline): ...
- With that ``TabularInline`` (instead of ``StackedInline``), the
- related objects are displayed in a more compact, table-based format:
- .. image:: _images/admin11t.png
- :alt: Add question page now has more compact choices
- Note that there is an extra "Delete?" column that allows removing rows added
- using the "Add another Choice" button and rows that have already been saved.
- Customize the admin change list
- ===============================
- Now that the Question admin page is looking good, let's make some tweaks to the
- "change list" page -- the one that displays all the questions in the system.
- Here's what it looks like at this point:
- .. image:: _images/admin04t.png
- :alt: Polls change list page
- By default, Django displays the ``str()`` of each object. But sometimes it'd be
- more helpful if we could display individual fields. To do that, use the
- :attr:`~django.contrib.admin.ModelAdmin.list_display` admin option, which is a
- list of field names to display, as columns, on the change list page for the
- object:
- .. code-block:: python
- :caption: ``polls/admin.py``
- class QuestionAdmin(admin.ModelAdmin):
- # ...
- list_display = ["question_text", "pub_date"]
- For good measure, let's also include the ``was_published_recently()`` method
- from :doc:`Tutorial 2 </intro/tutorial02>`:
- .. code-block:: python
- :caption: ``polls/admin.py``
- class QuestionAdmin(admin.ModelAdmin):
- # ...
- list_display = ["question_text", "pub_date", "was_published_recently"]
- Now the question change list page looks like this:
- .. image:: _images/admin12t.png
- :alt: Polls change list page, updated
- You can click on the column headers to sort by those values -- except in the
- case of the ``was_published_recently`` header, because sorting by the output
- of an arbitrary method is not supported. Also note that the column header for
- ``was_published_recently`` is, by default, the name of the method (with
- underscores replaced with spaces), and that each line contains the string
- representation of the output.
- You can improve that by using the :func:`~django.contrib.admin.display`
- decorator on that method (extending the :file:`polls/models.py` file that was
- created in :doc:`Tutorial 2 </intro/tutorial02>`), as follows:
- .. code-block:: python
- :caption: ``polls/models.py``
- from django.contrib import admin
- class Question(models.Model):
- # ...
- @admin.display(
- boolean=True,
- ordering="pub_date",
- description="Published recently?",
- )
- def was_published_recently(self):
- now = timezone.now()
- return now - datetime.timedelta(days=1) <= self.pub_date <= now
- For more information on the properties configurable via the decorator, see
- :attr:`~django.contrib.admin.ModelAdmin.list_display`.
- Edit your :file:`polls/admin.py` file again and add an improvement to the
- ``Question`` change list page: filters using the
- :attr:`~django.contrib.admin.ModelAdmin.list_filter`. Add the following line to
- ``QuestionAdmin``::
- list_filter = ["pub_date"]
- That adds a "Filter" sidebar that lets people filter the change list by the
- ``pub_date`` field:
- .. image:: _images/admin13t.png
- :alt: Polls change list page, updated
- The type of filter displayed depends on the type of field you're filtering on.
- Because ``pub_date`` is a :class:`~django.db.models.DateTimeField`, Django
- knows to give appropriate filter options: "Any date", "Today", "Past 7 days",
- "This month", "This year".
- This is shaping up well. Let's add some search capability::
- search_fields = ["question_text"]
- That adds a search box at the top of the change list. When somebody enters
- search terms, Django will search the ``question_text`` field. You can use as many
- fields as you'd like -- although because it uses a ``LIKE`` query behind the
- scenes, limiting the number of search fields to a reasonable number will make
- it easier for your database to do the search.
- Now's also a good time to note that change lists give you free pagination. The
- default is to display 100 items per page. :attr:`Change list pagination
- <django.contrib.admin.ModelAdmin.list_per_page>`, :attr:`search boxes
- <django.contrib.admin.ModelAdmin.search_fields>`, :attr:`filters
- <django.contrib.admin.ModelAdmin.list_filter>`, :attr:`date-hierarchies
- <django.contrib.admin.ModelAdmin.date_hierarchy>`, and
- :attr:`column-header-ordering <django.contrib.admin.ModelAdmin.list_display>`
- all work together like you think they should.
- Customize the admin look and feel
- =================================
- Clearly, having "Django administration" at the top of each admin page is
- ridiculous. It's just placeholder text.
- You can change it, though, using Django's template system. The Django admin is
- powered by Django itself, and its interfaces use Django's own template system.
- .. _ref-customizing-your-projects-templates:
- Customizing your *project's* templates
- --------------------------------------
- Create a ``templates`` directory in your ``djangotutorial`` directory.
- Templates can live anywhere on your filesystem that Django can access. (Django
- runs as whatever user your server runs.) However, keeping your templates within
- the project is a good convention to follow.
- Open your settings file (:file:`mysite/settings.py`, remember) and add a
- :setting:`DIRS <TEMPLATES-DIRS>` option in the :setting:`TEMPLATES` setting:
- .. code-block:: python
- :caption: ``mysite/settings.py``
- TEMPLATES = [
- {
- "BACKEND": "django.template.backends.django.DjangoTemplates",
- "DIRS": [BASE_DIR / "templates"],
- "APP_DIRS": True,
- "OPTIONS": {
- "context_processors": [
- "django.template.context_processors.request",
- "django.contrib.auth.context_processors.auth",
- "django.contrib.messages.context_processors.messages",
- ],
- },
- },
- ]
- :setting:`DIRS <TEMPLATES-DIRS>` is a list of filesystem directories to check
- when loading Django templates; it's a search path.
- .. admonition:: Organizing templates
- Just like the static files, we *could* have all our templates together, in
- one big templates directory, and it would work perfectly well. However,
- templates that belong to a particular application should be placed in that
- application's template directory (e.g. ``polls/templates``) rather than the
- project's (``templates``). We'll discuss in more detail in the
- :doc:`reusable apps tutorial </intro/reusable-apps>` *why* we do this.
- Now create a directory called ``admin`` inside ``templates``, and copy the
- template ``admin/base_site.html`` from within the default Django admin
- template directory in the source code of Django itself
- (:source:`django/contrib/admin/templates`) into that directory.
- .. admonition:: Where are the Django source files?
- If you have difficulty finding where the Django source files are located
- on your system, run the following command:
- .. console::
- $ python -c "import django; print(django.__path__)"
- Then, edit the file and replace
- ``{{ site_header|default:_('Django administration') }}`` (including the curly
- braces) with your own site's name as you see fit. You should end up with
- a section of code like:
- .. code-block:: html+django
- {% block branding %}
- <div id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></div>
- {% if user.is_anonymous %}
- {% include "admin/color_theme_toggle.html" %}
- {% endif %}
- {% endblock %}
- We use this approach to teach you how to override templates. In an actual
- project, you would probably use
- the :attr:`django.contrib.admin.AdminSite.site_header` attribute to more easily
- make this particular customization.
- This template file contains lots of text like ``{% block branding %}``
- and ``{{ title }}``. The ``{%`` and ``{{`` tags are part of Django's
- template language. When Django renders ``admin/base_site.html``, this
- template language will be evaluated to produce the final HTML page, just like
- we saw in :doc:`Tutorial 3 </intro/tutorial03>`.
- Note that any of Django's default admin templates can be overridden. To
- override a template, do the same thing you did with ``base_site.html`` -- copy
- it from the default directory into your custom directory, and make changes.
- Customizing your *application's* templates
- ------------------------------------------
- Astute readers will ask: But if :setting:`DIRS <TEMPLATES-DIRS>` was empty by
- default, how was Django finding the default admin templates? The answer is
- that, since :setting:`APP_DIRS <TEMPLATES-APP_DIRS>` is set to ``True``,
- Django automatically looks for a ``templates/`` subdirectory within each
- application package, for use as a fallback (don't forget that
- ``django.contrib.admin`` is an application).
- Our poll application is not very complex and doesn't need custom admin
- templates. But if it grew more sophisticated and required modification of
- Django's standard admin templates for some of its functionality, it would be
- more sensible to modify the *application's* templates, rather than those in the
- *project*. That way, you could include the polls application in any new project
- and be assured that it would find the custom templates it needed.
- See the :ref:`template loading documentation <template-loading>` for more
- information about how Django finds its templates.
- Customize the admin index page
- ==============================
- On a similar note, you might want to customize the look and feel of the Django
- admin index page.
- By default, it displays all the apps in :setting:`INSTALLED_APPS` that have been
- registered with the admin application, in alphabetical order. You may want to
- make significant changes to the layout. After all, the index is probably the
- most important page of the admin, and it should be easy to use.
- The template to customize is ``admin/index.html``. (Do the same as with
- ``admin/base_site.html`` in the previous section -- copy it from the default
- directory to your custom template directory). Edit the file, and you'll see it
- uses a template variable called ``app_list``. That variable contains every
- installed Django app. Instead of using that, you can hard-code links to
- object-specific admin pages in whatever way you think is best.
- When you're comfortable with the admin, read :doc:`part 8 of this
- tutorial </intro/tutorial08>` to learn how to use third-party packages.
|