The Secret of Django Auto Generated Admin Docs

A few weeks ago, I contributed to Django with a couple of patches. One of them consisted of an enhancement to Django’s auto generated admin documentation for admin sites. I got to know how this feature really works.

Read on if you want to get a clear picture of how you can make more effective use of it.

Django’s Automated Generator

In this post, I’ll assume that you are working with Python 3.

This past week I had a chance to learn more about the internals of Django’s automated admin documentation generator.

Among a plethora of cool stuff, the automated generation of admin sites and the documentation for your project’s code stands out when comparing Django to similar web frameworks.

I’ll walk you through some of Django’s internals to understand where the magic for automated docs generation happens.

Setting Up Your Environment

Create the directory where your project will live and move there (e.g., cd).

Create a virtual environment by executing the following command,

$ python3 -m venv yourvenvname

To activate the environment and install Django, execute the following commands,

$ source yourvenvname/bin/activate
$ pip install django

Writing Your Application

To start a Django project, execute the following command from within the root dir for your project,

$ django-admin startproject yourprojectname

Move into the directory created by the previous command. Create an app named blog.
You will be working under this context.

Now, run the following command to create the app,

$ python manage.py startapp blog

NOTE: for an explanation of what is considered a project and what is considered an app, go to the official Django Docs.

Writing a Small Django App

Now, it is time to write some code,

  • Create the models for the blog app
  • Configure the urls to which the app will respond
  • Set up the database (i.e., we will be using the default configuration with SQLite)
  • Set up the admin site and docs.

I will not be talking about views, as this is not within the scope of this post.

Start by going to the blog/models.py file and write,

import datetime

from django.db import models

# Create your models here.
class Author(models.Model):
    name = models.CharField(max_length=100)

    # This defines how the object will be represented.
    def __str__(self):
        return self.name

class BlogPost(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    post_title = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    # This defines how the object will be represented.
    def __str__(self):
        return self.post_title

    # States if the blog post was published less than a day ago.
    def published_today(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

Create two models for your blog app: a BlogPost and Author.

The Author model has only one attribute for its name. The BlogPost model has several: a post title, publishing date, an author (which is an Author instance), and a method that will tell if the blog post was published today.

Don’t worry about adding the routes for the blog app as you will not be working with them for this post.

Setting Up the Database

Before running the migrations to set up the database, add the blog app to the INSTALLED_APPS list in the settings file. Go to the file settings.py in your project’s root dir and add the following item to the INSTALLED_APPS list,

'blog.apps.BlogConfig',

Run the following command to create the appropriate migrations for your Blog app, and run them to setup the database,

$ python manage.py createmigrations blog
$ python manage.py migrate

Your database is now set and ready.

Building An Admin Panel

Add the proper configuration to build the admin site. Go to the urls.py file in the project’s root directory. Make sure the following line is included in your urlpatterns list,

path('admin/', admin.site.url),

You can now navigate to the admin site, but first, create an admin user. To do it, run the following command (i.e., it will prompt you for the appropriate credentials),

$ python manage.py createsuperuser

Run the development server,

$ python manage.py runserver

You’re all set to visit the admin site and log in with the credentials you just provided. You will be logged into the home admin site.

Adding App Documentation to Admin Site

Let’s see how to add the documentation for your app to your admin site.

First you will have to install the docutils Python module. Navigate to this link for how to install it.

After the installation, add the following two lines of code,

  • In settings.py, in the INSTALLED_APPS list, add django.contrib.admindocs.
  • In your urls.py, add path('admin/doc/',include('django.contrib.admindocs.urls')) to your urlpatterns list. Make sure to add it before the ‘admin/’ route.

Now you can visit the documentation section and you’ll see there is a section for models.

Navigate to the models section and select the BlogPost model to see its details.

How to Improve the Description of Fields and Methods

It’s as easy as adding docstrings to your methods and human readable alternative strings and help text to your model fields.

Try it by adding or removing comments enclosed by double-quotes at the beginning of your methods, and by adding a help_text attribute to your field definitions.

How Does Django Generate its Model Documentation?

Let’s go to the Django source.

In django.contrib.admindocs.views.py you will find there are a couple of classes named ModelIndexView and ModelDetailView. These are the ones in charge of collecting and packing your model’s info in order to pass it to the views.

As you can see in the following code, the ModelIndexView retrieves models metadata to create a list that will be passed on to the view.

class ModelIndexView(BaseAdminDocsView):
    template_name = 'admin_doc/model_index.html'

    def get_context_data(self, **kwargs):
        m_list = [m._meta for m in apps.get_models()]
        return super().get_context_data(**{**kwargs, 'models': m_list})

In ModelDetailView you can see that the model fields (attributes) and methods are packed in lists.

For attributes, a dictionary for each one is created with the following keys: name, data_type, verbose. The last key is for human readable or extended descriptions.

For methods, same as fields, a dictionary is created with the keys name, arguments, and verbose.

Something to point out is the fact that this class will consider any method without arguments as a Field.

# Gather fields/field descriptions.
        fields = []
        for field in opts.fields:
            # ForeignKey is a special case since the field will actually be a
            # descriptor that returns the other object
            if isinstance(field, models.ForeignKey):
                data_type = field.remote_field.model.__name__
                app_label = field.remote_field.model._meta.app_label
                verbose = utils.parse_rst(
                    (_("the related `%(app_label)s.%(data_type)s` object") % {
                        'app_label': app_label, 'data_type': data_type,
                    }),
                    'model',
                    _('model:') + data_type,
                )
            else:
                data_type = get_readable_field_data_type(field)
                verbose = field.verbose_name
            fields.append({
                'name': field.name,
                'data_type': data_type,
                'verbose': verbose or '',
                'help_text': field.help_text,
            })
# Gather model methods.
        for func_name, func in model.__dict__.items():
            if inspect.isfunction(func):
                try:
                    for exclude in MODEL_METHODS_EXCLUDE:
                        if func_name.startswith(exclude):
                            raise StopIteration
                except StopIteration:
                    continue
                verbose = func.__doc__
                verbose = verbose and (
                    utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.model_name)
                )
                # If a method has no arguments, show it as a 'field', otherwise
                # as a 'method with arguments'.
                if func_has_no_args(func) and not func_accepts_kwargs(func) and not func_accepts_var_args(func):
                    fields.append({
                        'name': func_name,
                        'data_type': get_return_data_type(func_name),
                        'verbose': verbose or '',
                    })

Finally, all this packed info regarding model methods and fields is passed to view templates through a method inherited by these classes from one of the classes in their lineage. You can see this in action in the django.contrib.admindocs.urls.py.

urlpatterns = [
    ...
    path(
        'models/',
        views.ModelIndexView.as_view(),
        name='django-admindocs-models-index',
    ),
    re_path(
        r'^models/(?P[^\.]+)\.(?P[^/]+)/$',
        views.ModelDetailView.as_view(),
        name='django-admindocs-models-detail',
    ),
    ...
]

Finally…

I hope this post has helped shed some light on the internals of Django’s admin docs to help you better understand how it is generated.

Also, the official documentation is a great place to visit if you want to know more about what you can do to improve your admin docs.

Have fun with Django and make the most out of it.

For further questions you can contact me at [email protected].

Focus Mode

Contact Request

Close

We will call you right away. All information is kept private