diff options
Diffstat (limited to 'docs/intro/tutorial07.txt')
| -rw-r--r-- | docs/intro/tutorial07.txt | 421 |
1 files changed, 421 insertions, 0 deletions
diff --git a/docs/intro/tutorial07.txt b/docs/intro/tutorial07.txt new file mode 100644 index 0000000000..ecfd90d7f2 --- /dev/null +++ b/docs/intro/tutorial07.txt @@ -0,0 +1,421 @@ +===================================== +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 the Django's +automatically-generated admin site that we first explored in :doc:`Tutorial 2 +</intro/tutorial02>`. + +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: + +.. snippet:: + :filename: 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 an 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: + +.. snippet:: + :filename: 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``. That's easy: + +.. snippet:: + :filename: 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" link next to "Question." Every object with a +``ForeignKey`` relationship to another gets this for free. When you click "Add +Another", you'll get a popup window with the "Add 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: + +.. snippet:: + :filename: 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. Note +that you can't remove the original three slots. 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; you just need to change +the ``ChoiceInline`` declaration to read: + +.. snippet:: + :filename: 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 +tuple of field names to display, as columns, on the change list page for the +object: + +.. snippet:: + :filename: polls/admin.py + + class QuestionAdmin(admin.ModelAdmin): + # ... + list_display = ('question_text', 'pub_date') + +Just for good measure, let's also include the ``was_published_recently()`` +method from :doc:`Tutorial 2 </intro/tutorial02>`: + +.. snippet:: + :filename: 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 giving that method (in :file:`polls/models.py`) a few +attributes, as follows: + +.. snippet:: + :filename: polls/models.py + + class Question(models.Model): + # ... + def was_published_recently(self): + return self.pub_date >= timezone.now() - datetime.timedelta(days=1) + was_published_recently.admin_order_field = 'pub_date' + was_published_recently.boolean = True + was_published_recently.short_description = 'Published recently?' + +For more information on these method properties, 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. + +That's easy to change, 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 project directory (the one that +contains ``manage.py``). 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: + +.. snippet:: + :filename: mysite/settings.py + + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + '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 belongs to a particular application, we should put in the + 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 +(``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: + + .. code-block:: console + + $ python -c "import django; print(django.__path__)" + +Then, just 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 %} + <h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1> + {% 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, just 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. + +What's next? +============ + +The beginner tutorial ends here. In the meantime, you might want to check out +some pointers on :doc:`where to go from here </intro/whatsnext>`. + +If you are familiar with Python packaging and interested in learning how to +turn polls into a "reusable app", check out :doc:`Advanced tutorial: How to +write reusable apps</intro/reusable-apps>`. |
