The recording for this lesson has not been published yet.
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.
blog/models.py
contains Post, Comment, Profile,
NewsletterSubscription and a Payment - and none of the last
three has anything to do with blogging.views.py of 600 lines is a
directory, not a module.Post.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.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"),
]
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",
]
INSTALLED_APPS from top to bottom. If two apps ship a file
called templates/base.html, the first one wins.
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
git switch -c refactor/app-split.python manage.py startapp comments and add it to
INSTALLED_APPS.from blog.models import Comment becomes
from comments.models import Comment.comments/urls.py and include() them.python manage.py check, then the test suite, then click through the
site.main and never do it in one commit. A
refactoring you cannot revert is not a refactoring, it is a gamble.
core (remember
manage.py, wsgi.py, asgi.py and
ROOT_URLCONF).common app with an empty models.py and register it
with a proper AppConfig.blog its own urls.py with app_name and
convert every hard-coded link in your templates to
{% url %}.python manage.py migrate --plan reports nothing to do.