Video coming soon

The recording for this lesson has not been published yet.

What we cover

  • One app is never enough: signs it is time to split
  • A core app for shared models and utilities
  • Feature apps: blog, accounts, comments
  • apps.py, default_auto_field and app labels
  • Moving code without breaking imports

Lesson notes

1. Where the tutorial left you

After the Django Girls tutorial your repository looks more or less like this. Everything that exists lives in a single app called blog, and the project package (mysite) holds the settings and the only URL configuration.

djangogirls/
├── manage.py
├── db.sqlite3
├── requirements.txt
├── mysite/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── blog/
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── forms.py
    ├── models.py
    ├── urls.py
    ├── views.py
    ├── migrations/
    └── templates/blog/

There is nothing wrong with this layout - for a project with one model and four views it is exactly right. The trouble starts when the second and the third feature arrive.

2. Signs that one app is no longer enough

  • The file names stop telling the truth. blog/models.py contains Post, Comment, Profile, NewsletterSubscription and a Payment - and none of the last three has anything to do with blogging.
  • You scroll to navigate. A views.py of 600 lines is a directory, not a module.
  • Nothing can be reused. You cannot copy the tagging code into your next project, because it is entangled with Post.
  • Every change touches the same files. That is how merge conflicts are born.

3. The layout we are moving to

myblog/
├── manage.py
├── pyproject.toml
├── requirements/
│   ├── base.txt
│   └── dev.txt
├── core/                      # project package: settings, root urls, asgi/wsgi
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── asgi.py
│   └── wsgi.py
├── common/                    # shared code: abstract models, mixins, template tags
│   ├── __init__.py
│   ├── apps.py
│   ├── models.py
│   └── templatetags/
├── blog/                      # posts, categories, tags
├── comments/                  # comments and moderation
├── accounts/                  # user model, registration, profiles
├── templates/                 # project-wide templates
│   └── base.html
└── static/

Two names deserve an explanation.

  • core is the project package - the folder created by django-admin startproject. It holds settings.py, the root urls.py and the WSGI/ASGI entry points. Nothing else. It is not an app and it is not in INSTALLED_APPS.
  • common is a real app that holds code shared by the other apps: abstract base models, small utilities, custom template tags. Some people call it core too - which is exactly why we renamed the project package, to keep the two ideas apart.

4. How a request travels through the new layout

Browser GET /blog/1/ core/urls.py include() per app accounts/urls.py blog/urls.py comments/urls.py blog.views post_detail

The root URLconf becomes a table of contents for the whole project - one line per app:

# core/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("accounts.urls", namespace="accounts")),
    path("comments/", include("comments.urls", namespace="comments")),
    path("", include("blog.urls", namespace="blog")),
]

Each app declares its own namespace, so {% url 'blog:post_detail' post.pk %} keeps working even if two apps have a view called detail.

# blog/urls.py
from django.urls import path

from . import views

app_name = "blog"

urlpatterns = [
    path("", views.post_list, name="post_list"),
    path("post/<int:pk>/", views.post_detail, name="post_detail"),
]

5. apps.py is not boilerplate

The tutorial never asked you to open apps.py. It is a small file, but it is the place where an app describes itself to Django.

# blog/apps.py
from django.apps import AppConfig


class BlogConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "blog"                 # the Python path - must be importable
    label = "blog"                # unique short name used in migrations
    verbose_name = "Blog posts"   # what the admin shows

    def ready(self):
        # The only correct place to import signal handlers.
        from . import signals  # noqa: F401

Register the config class, not the package, so that ready() is actually used:

# core/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # your apps
    "common.apps.CommonConfig",
    "accounts.apps.AccountsConfig",
    "blog.apps.BlogConfig",
    "comments.apps.CommentsConfig",
]

6. Moving a model without losing your data

Cutting Comment out of blog/models.py and pasting it into comments/models.py looks harmless, but Django sees a deleted table and a new one - and migrate would happily drop your data. There are two safe ways.

Option A - keep the existing table. Point the model at the old table name:

# comments/models.py
class Comment(models.Model):
    ...

    class Meta:
        db_table = "blog_comment"

Then generate the migrations and wrap them so that only Django's internal state changes, not the database:

# comments/migrations/0001_initial.py
from django.db import migrations, models


class Migration(migrations.Migration):
    initial = True
    dependencies = [("blog", "0003_auto_20240101_1200")]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[],  # the table already exists - touch nothing
            state_operations=[
                migrations.CreateModel(
                    name="Comment",
                    fields=[...],
                    options={"db_table": "blog_comment"},
                ),
            ],
        ),
    ]

Option B - a real rename. Let Django create comments_comment, copy the rows in a data migration, then delete the old table. More work, cleaner result. We come back to data migrations in lesson 2.2.

Whatever you do, always look at the SQL before you run it:

python manage.py makemigrations --dry-run --verbosity 3
python manage.py sqlmigrate comments 0001
python manage.py migrate --plan

7. The move, step by step

  1. Create a branch: git switch -c refactor/app-split.
  2. python manage.py startapp comments and add it to INSTALLED_APPS.
  3. Move the model, the form, the view and the template - one object at a time, committing after each one.
  4. Fix the imports. from blog.models import Comment becomes from comments.models import Comment.
  5. Move the URLs into comments/urls.py and include() them.
  6. Run python manage.py check, then the test suite, then click through the site.

8. Exercise

  1. Rename your project package to core (remember manage.py, wsgi.py, asgi.py and ROOT_URLCONF).
  2. Create a common app with an empty models.py and register it with a proper AppConfig.
  3. Give blog its own urls.py with app_name and convert every hard-coded link in your templates to {% url %}.
  4. Confirm that python manage.py migrate --plan reports nothing to do.

Further reading

After this lesson you will

  • Split the blog project into several focused apps
  • Decide what belongs in a shared core app
  • Configure apps properly with AppConfig