Django from first principles, part 12

MP 105: Reorganizing a project to manage increasing complexity.

Note: This is the 12th post in a series about building a full Django project, starting with a single file. This series is free to everyone, as soon as each post comes out.

In the last post we wrote a script to generate sample data for the entire project. That work lets us see much more clearly what the site will look like to actual users, with realistic amounts of data. We're at the point now where we need to implement user accounts, so people can create blogs and write their own posts.

Before implementing user accounts, we really ought to clean up the organization of the project. Currently, code for dealing with blogs and posts is spread throughout the project. In this post, we'll move all that code to the blogs/ directory, so it will be cleanly separated from code that's specific to managing user accounts. This will make it much easier to understand the overall structure of the project is it continues to grow in complexity.

The work we're about to do won't change the functionality of BlogMaker Lite at all. But this is a key part of the entire series; this is the point where we're transitioning from what grew out of the single-file approach, toward the standard structure of most Django projects.

The structure of blogmaker_lite.py

Much of the code that needs to be moved is in the main project file, blogmaker_lite.py. Here's the current structure of that file:

blogmaker_lite.py, with colored rectangles highlighting 5 main sections: import statements, admin registration, view functions, URL patterns, and WSGI handler
The file blogmaker_lite.py hass about 50 lines of code, but all those lines are grouped into 5 main sections.

Most of what's in here is specific to managing blogs, but not quite all of it. We'll take each section that's specific to blogs, and move it to a more appropriate place in the project. By the time we're finished, blogmaker_lite.py will be much shorter, and the project overall will be much better organized.

Registering with the admin site

First, let's move the code that registers the blog-related models with the admin site. This is only one line of code, but it's still specific to blogs. We'll move this to a file called admin.py, in the blogs/ directory:

from django.contrib import admin

from .models import Blog, BlogPost

admin.site.register((Blog, BlogPost))
blogs/admin.py

This is a small file, but it puts the work of registering blog models into the part of the project that deals with blogs. After creating this file, you can remove the admin registration line from blogmaker_lite.py.

Moving the view functions

All the view functions that have been defined so far serve pages that are specific to blogs. Let's move these functions to a new file called views.py, again in the blogs/ directory:

from django.shortcuts import render

from .models import Blog, BlogPost

def index(request):
    ...

def blogs(request):
    ...

def blog(request, blog_id):
    ...

def post(request, post_id):
    ...
blogs/views.py

Almost all this code is just moved to the new file. The only change in the code is in the import statement, because the file is now in the same directory as models.py.

We need to make some changes to blogmaker_lite.py beyond just removing the view functions:

from django.urls import path
from django.core.handlers.wsgi import WSGIHandler
from django.contrib import admin

from blogs import views

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blogs/", views.blogs, name="blogs"),
    path("blogs/<int:blog_id>/", views.blog, name="blog"),
    path("posts/<int:post_id>/", views.post, name="post"),
    path("", views.index, name="index"),
]

application = WSGIHandler()
blogmaker_lite.py

The URL patterns reference specific view functions, so we need to import the views module that we just created. Each URL pattern referencing one of these view functions needs to specify that the function is now in the views module.

Moving URL patterns

The file blogmaker_lite.py is much shorter now. The only remaining blog-specific code involves URL patterns. Let's move those patterns to a new file called urls.py, once again in the blogs/ directory:

from django.urls import path

from . import views

urlpatterns = [
    path("blogs/", views.blogs, name="blogs"),
    path("blogs/<int:blog_id>/", views.blog, name="blog"),
    path("posts/<int:post_id>/", views.post, name="post"),
    path("", views.index, name="index"),
]
blogs/urls.py

The views module is in the same directory as urls.py, so the import statement needs to be adjusted to reflect that.

This requires some changes in blogmaker_lite.py as well:

from django.urls import path, include
from django.contrib import admin
from django.core.handlers.wsgi import WSGIHandler

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("blogs.urls")),
]

application = WSGIHandler()
blogmaker_lite.py

We need to import the include() function from the django.urls module. This function lets you include a set of URL patterns from another module. The list urlpatterns is now much simpler; we're just including the URLs required to serve the admin site, and the URLs required to serve all the pages related to blogs. If you want to know exactly what those URLs are, you can go to those specific files.

Notice there's no longer any code in this file that's specific to blogs, other than the line that includes the blog-specific URL patterns. The main file now does just two things: define which URLs are included in the overall project, and designate a WSGI handler for the server.

Moving templates

That's everything we need to take care of in blogmaker_lite.py. Now let's take a look at the overall project structure:

$  tree -L 2
├── blogmaker_lite.py
├── blogs
│   ├── admin.py
│   ├── migrations
│   ├── models.py
│   ├── urls.py
│   └── views.py
├── css
│   ├── chota.css
│   └── custom.css
├── db.sqlite3
├── generate_sample_data.py
├── manage.py
├── model_factories.py
├── settings.py
└── templates
    ├── base.html
    ├── blog.html
    ├── blogs.html
    ├── index.html
    └── post.html

There's a lot more here than when we started the project with just one file! But there's a good reason for everything that's included in the project at this point.

There's one main component of the project that's not really where it should be. The templates are at the root project level, but they're all focused on serving blog-related pages. Let's move those into the blogs/ directory.

Because of the way Django's template discovery mechanism works, these files should be moved to the directory path blogs/templates/blogs/. So, make a new folder called templates/ inside the blogs/ directory. Inside that blogs/templates/ directory, make a second folder named blogs/. Copy all the template files to that folder. Once they're copied over, delete the original templates/ directory in the project's root folder.

You should have the following in the blogs/ folder after this change: 1

$  tree -L 3 blogs
blogs
├── admin.py
├── migrations
│   ├── ...
├── models.py
├── templates
│   └── blogs
│       ├── base.html
│       ├── blog.html
│       ├── blogs.html
│       ├── index.html
│       └── post.html
├── urls.py
└── views.py

We need to update the path to these templates in blogs/views.py:

from django.shortcuts import render

from .models import Blog, BlogPost

def index(request):
    return render(request, "blogs/index.html")

def blogs(request):
    ...
    return render(request, "blogs/blogs.html", context)

def blog(request, blog_id):
    ...
    return render(request, "blogs/blog.html", context)

def post(request, post_id):
    ...
    return render(request, "blogs/post.html", context)
blogs/views.py

These changes tell Django to look for templates in the appropriate location when rendering pages.

There are two changes we need to make the templates. We need to update the path to the base template in each of the {% extends %} tags. Here's the change that needs to be made in index.html:

{% extends "blogs/base.html" %}

This is just like the change in the calls to render() in the view functions. When the template is being used to render the page, Django looks for the base.html template in the blogs/ directory. This change needs to be made in all the other templates that extend base.html as well.

Each of the {% url %} tags needs a similar update. For example, here's the link to the home page in the navigation bar, in base.html:

{% url 'blogs:index' %}

When Django constructs URLs from these tags, it will now look for the URL pattern named index, in the URL patterns found in the blogs/ directory. This change needs to be made to all {% url %} tags in each template.

Finally, we need to tell Django that all the URLs associated with blogs should be referred to by that name. Here's the one-line change to urls.py:

from django.urls import path

from . import views

app_name = "blogs"
urlpatterns = [
    ...
]

The app_name setting here will allow us to distinguish URL patterns associated with blogs from other groups of URL patterns in the project.

Conclusions

Here's the final project structure after all this reorganization:

$  tree -L 4
├── blogmaker_lite.py
├── blogs
│   ├── admin.py
│   ├── migrations
│   │   └── ...
│   ├── models.py
│   ├── templates
│   │   └── blogs
│   │       ├── base.html
│   │       └── ...
│   ├── urls.py
│   └── views.py
├── css
│   ├── chota.css
│   └── custom.css
├── db.sqlite3
├── generate_sample_data.py
├── manage.py
├── model_factories.py
└── settings.py

The project is in a much better state for bringing in new functionality now. Almost all the blog-specific code is in the blogs/ directory. When we add code related to user accounts, we'll put most of that code into a directory called accounts/. As the project continues to grow, it will retain a consistent structure that makes any increases in complexity more manageable.

It's also worth revisiting the image of blogmaker_lite.py from the beginning of this post. Here's a slightly different version, using the names of the files where we moved each block of code:

blogmaker_lite.py with sections labeled admin.py, views.py, urls.py, and wsgi.py
Each of the sections we started with has been moved to a file that matches the way most Django projects are structured.

These are files you'll see in almost every Django project you work on. When you see those files, remember it's just an approach to managing complexity in real-world projects. It was a fair bit of work to restructure the project, which is another reason Django projects typically start out with this kind of layout, even though all these files aren't always needed right away. Most real-world projects end up with at least this level of complexity, and if you know that's where you're headed it's a lot easier to start out with that structure in the first place. It's just not necessarily the best way to build an understanding of why that structure is a good idea.

The project structure we now have is also what makes it possible to build reusable components, or apps, in Django. With a little more work, you could package up the blog-specific code so that it could be imported into any Django project. That's a powerful concept, and it's what enables the entire third-party library ecosystem in the Django world.

Resources

You can find the code files from this post in the django-first-principles GitHub repository. The commits from this post are on the part_12 branch. Commits for this branch start at 500dd5, with the message Move admin registration to admin.py.


1

The path blogs/templates/blogs/base.html certainly looks redundant. It's set up this way because Django looks for a folder called templates/ inside folders such as blogs/. It's helpful to think of blogs/ as an app; that's how Django thinks of it. If an app's template files are placed directly in the app_name/templates/ directory, Django won't easily be able to distinguish templates from different apps, unless they have unique names.

In smaller projects this is unlikely to be an issue. But in mature, real-world projects, this can easily become an issue. This is especially true when you start to include third-party apps in a project. For a good, concise explanation of this, see the Template namespacing note in the official tutorial.

If you're reading the footnotes, you might also be interested in knowing that all this template discovery behavior is enabled by the APP_DIRS template setting:

TEMPLATES=[
    {
        "BACKEND": ...
        "DIRS": [Path(__file__).parent / "templates"],
        "APP_DIRS": True,
        ...
    }
]
settings.py

When APP_DIRS is set to True, Django looks in each app's folder for a templates/ directory, as described above. You could add these directories to DIRS manually, but the APP_DIRS setting allows templates to be discovered automatically.