Chapter 2: Django MTV Architecture
2.2 Django Application (App)
What is a Django Application
A Django application (App) is a Python package that contains specific functionality models, views, templates, and URL configurations. A Django project can contain multiple applications, each responsible for different functional modules.
Project vs Application
- Project: The entire web application collection, containing configuration and multiple applications
- Application: A functional module within the project, such as blog, user management, comment system, etc.
mysite/ # Django project
├── manage.py
├── mysite/ # Project configuration
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── blog/ # Blog application
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── views.py
│ └── urls.py
└── users/ # User management application
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── views.py
└── urls.pyCreate Application Command
python manage.py startapp <app_name>Create Blog Application
# Create application in project root directory
python manage.py startapp blog
# View created application structure
tree blog/ # Windows/Linux
# or
ls -la blog/Created application structure:
blog/
├── __init__.py # Python package identifier file
├── admin.py # Django admin configuration
├── apps.py # Application configuration
├── migrations/ # Database migration file directory
│ └── __init__.py
├── models.py # Data models
├── tests.py # Test files
└── views.py # View functionsCreate Multiple Applications
Can only create one application at a time.
# Create user management application
python manage.py startapp users
# Create comment system application
python manage.py startapp comments
# Create API application
python manage.py startapp apiApplication Structure Details
__init__.py
Empty file, identifies this as a Python package.
apps.py
Application configuration file:
# blog/apps.py
from django.apps import AppConfig
class BlogConfig(AppConfig):
"""Blog application configuration"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'blog'
verbose_name = 'Blog Management'
# Optional
def ready(self):
"""Code executed when application starts"""
# Import signal handlers
import blog.signalsmodels.py
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils import timezone
class Category(models.Model):
"""Article category model"""
name = models.CharField(max_length=100, verbose_name="Category Name")
slug = models.SlugField(unique=True, verbose_name="URL Alias")
description = models.TextField(blank=True, verbose_name="Category Description")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created Time")
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('blog:category_detail', kwargs={'slug': self.slug})
class Tag(models.Model):
"""Article tag model"""
name = models.CharField(max_length=50, unique=True, verbose_name="Tag Name")
slug = models.SlugField(unique=True, verbose_name="URL Alias")
class Meta:
verbose_name = "Tag"
verbose_name_plural = "Tags"
ordering = ['name']
def __str__(self):
return self.name
class PublishedManager(models.Manager):
"""Published article manager"""
def get_queryset(self):
return super().get_queryset().filter(
status='published',
publish_date__lte=timezone.now()
)
class Article(models.Model):
"""Article model"""
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]
title = models.CharField(max_length=200, verbose_name="Title")
slug = models.SlugField(unique_for_date='publish_date', verbose_name="URL Alias")
content = models.TextField(verbose_name="Content")
excerpt = models.TextField(max_length=300, blank=True, verbose_name="Excerpt")
# Relationship fields
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Author")
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Category")
tags = models.ManyToManyField(Tag, blank=True, verbose_name="Tags")
# Status and time fields
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', verbose_name="Status")
publish_date = models.DateTimeField(default=timezone.now, verbose_name="Publish Time")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created Time")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated Time")
# Statistics fields
view_count = models.PositiveIntegerField(default=0, verbose_name="View Count")
# Managers
objects = models.Manager() # Default manager
published = PublishedManager() # Custom manager
class Meta:
verbose_name = "Article"
verbose_name_plural = "Articles"
ordering = ['-publish_date']
indexes = [
models.Index(fields=['status', 'publish_date']),
models.Index(fields=['category', 'status']),
]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:article_detail', kwargs={
'year': self.publish_date.year,
'month': self.publish_date.month,
'day': self.publish_date.day,
'slug': self.slug
})
def save(self, *args, **kwargs):
"""Override save method, auto generate excerpt"""
if not self.excerpt:
self.excerpt = self.content[:200] + '...' if len(self.content) > 200 else self.content
super().save(*args, **kwargs)views.py
# blog/views.py
from django.shortcuts import render, get_object_or_404
from django.core.paginator import Paginator
from django.db.models import Q, F, Count
from django.utils import timezone
from django.views.generic import ListView, DetailView
from .models import Article, Category, Tag
def home(request):
"""Home page view"""
# Get latest published articles
latest_articles = Article.published.all()[:5]
# Get popular articles (by view count)
popular_articles = Article.published.order_by('-view_count')[:5]
# Get all categories
categories = Category.objects.annotate(
article_count=Count('article', filter=Q(article__status='published'))
).filter(article_count__gt=0)
context = {
'latest_articles': latest_articles,
'popular_articles': popular_articles,
'categories': categories,
}
return render(request, 'blog/home.html', context)
class ArticleListView(ListView):
"""Article list view"""
model = Article
template_name = 'blog/article_list.html'
context_object_name = 'articles'
paginate_by = 10
queryset = Article.published.all().select_related('author', 'category')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
context['popular_tags'] = Tag.objects.annotate(
article_count=Count('article')
).order_by('-article_count')[:10]
return context
class ArticleDetailView(DetailView):
"""Article detail view"""
model = Article
template_name = 'blog/article_detail.html'
context_object_name = 'article'
def get_queryset(self):
return Article.published.all()
def get_object(self, queryset=None):
obj = super().get_object(queryset)
# Increase view count
Article.objects.filter(pk=obj.pk).update(view_count=F('view_count') + 1)
return obj
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Get related articles
context['related_articles'] = Article.published.filter(
category=self.object.category
).exclude(pk=self.object.pk)[:5]
return context
def category_detail(request, slug):
"""Category detail view"""
category = get_object_or_404(Category, slug=slug)
articles = Article.published.filter(category=category)
# Pagination
paginator = Paginator(articles, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'category': category,
'articles': page_obj,
'is_paginated': page_obj.has_other_pages(),
'page_obj': page_obj,
}
return render(request, 'blog/category_detail.html', context)
def search(request):
"""Search function view"""
query = request.GET.get('q', '')
articles = []
if query:
articles = Article.published.filter(
Q(title__icontains=query) |
Q(content__icontains=query) |
Q(tags__name__icontains=query)
).distinct()
# Pagination
paginator = Paginator(articles, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'query': query,
'articles': page_obj,
'is_paginated': page_obj.has_other_pages(),
'page_obj': page_obj,
'total_results': paginator.count,
}
return render(request, 'blog/search_results.html', context)select_related
- Without select_related, produces N+1 queries (1 get articles + N get authors)
articles = Article.objects.all()
for article in articles:
print(article.author.name) # Each loop queries database for author- With select_related, produces only 1 query (uses JOIN to get all data at once)
articles = Article.objects.select_related('author').all()
for article in articles:
print(article.author.name) # Author info already obtained in first queryUsage scenarios
- ForeignKey relationships
- OneToOneField relationships
- Forward relationships (from main object to related object)
Not suitable for
- ManyToManyField relationships → use prefetch_related
- Reverse relationships (from related object to main object)
admin.py
# blog/admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Category, Tag, Article
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
"""Category management"""
list_display = ['name', 'slug', 'article_count', 'created_at']
list_filter = ['created_at']
search_fields = ['name', 'description']
prepopulated_fields = {'slug': ('name',)}
def article_count(self, obj):
"""Show article count in category"""
count = obj.article_set.count()
return format_html('<span style="color: blue;">{}</span>', count)
article_count.short_description = 'Article Count'
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
"""Tag management"""
list_display = ['name', 'slug', 'article_count']
search_fields = ['name']
prepopulated_fields = {'slug': ('name',)}
def article_count(self, obj):
return obj.article_set.count()
article_count.short_description = 'Article Count'
class TagInline(admin.TabularInline):
"""Article tag inline editing"""
model = Article.tags.through
extra = 1
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
"""Article management"""
list_display = ['title', 'author', 'category', 'status', 'view_count', 'publish_date', 'created_at']
list_filter = ['status', 'category', 'author', 'created_at', 'publish_date']
search_fields = ['title', 'content']
prepopulated_fields = {'slug': ('title',)}
raw_id_fields = ['author']
date_hierarchy = 'publish_date'
ordering = ['-created_at']
# Field grouping
fieldsets = (
('Basic Info', {
'fields': ('title', 'slug', 'author', 'category', 'status')
}),
('Content', {
'fields': ('content', 'excerpt')
}),
('Publish Settings', {
'fields': ('publish_date',),
'classes': ('collapse',)
}),
('Statistics', {
'fields': ('view_count',),
'classes': ('collapse',)
}),
)
# Filter
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)
def save_model(self, request, obj, form, change):
"""Auto set author when saving"""
if not change: # New article
obj.author = request.user
super().save_model(request, obj, form, change)
# Batch operations
actions = ['make_published', 'make_draft']
def make_published(self, request, queryset):
"""Batch publish articles"""
updated = queryset.update(status='published')
self.message_user(request, f'{updated} articles published.')
make_published.short_description = 'Publish selected articles'
def make_draft(self, request, queryset):
"""Batch set as draft"""
updated = queryset.update(status='draft')
self.message_user(request, f'{updated} articles set as draft.')
make_draft.short_description = 'Set selected articles as draft'urls.py
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
# Home page
path('', views.home, name='home'),
# Article related
path('articles/', views.ArticleListView.as_view(), name='article_list'),
path('article/<int:year>/<int:month>/<int:day>/<slug:slug>/',
views.ArticleDetailView.as_view(), name='article_detail'),
# Category related
path('category/<slug:slug>/', views.category_detail, name='category_detail'),
# Search
path('search/', views.search, name='search'),
]Application Registration
After creating an application, register it in the project's settings.py:
# mysite/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Custom applications
'blog.apps.BlogConfig', # Recommended way, using application configuration class
# Or shorthand: 'blog',
]Application Configuration Class Advantages
Using the full application configuration class path has these advantages:
- Clarity: Clearly specifies which configuration class to use
- Customization: Can add custom configurations in
BlogConfigclass - Signal handling: Can register signal handlers in
ready()method
Simple Blog Application Example
Let's create a complete blog application example:
1. Create Template Directory
# Create template directory in application directory
mkdir -p blog/templates/blog2. Create Base Template
<!-- blog/templates/blog/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Blog{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.sidebar {
background-color: #f8f9fa;
min-height: calc(100vh - 56px);
}
.article-meta {
color: #6c757d;
font-size: 0.9em;
}
.tag {
display: inline-block;
background-color: #e9ecef;
color: #495057;
padding: 0.25rem 0.5rem;
margin-right: 0.5rem;
border-radius: 0.25rem;
text-decoration: none;
font-size: 0.8em;
}
.tag:hover {
background-color: #dee2e6;
color: #495057;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'blog:home' %}">My Blog</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'blog:home' %}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'blog:article_list' %}">Articles</a>
</li>
</ul>
<form class="d-flex" method="get" action="{% url 'blog:search' %}">
<input class="form-control me-2" type="search" name="q" placeholder="Search articles..." value="{{ request.GET.q }}">
<button class="btn btn-outline-light" type="submit">Search</button>
</form>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<main class="col-md-9">
{% block content %}
{% endblock %}
</main>
<aside class="col-md-3 sidebar p-3">
{% block sidebar %}
<div class="mb-4">
<h5>Categories</h5>
{% for category in categories %}
<div>
<a href="{{ category.get_absolute_url }}" class="text-decoration-none">
{{ category.name }}
</a>
</div>
{% endfor %}
</div>
<div class="mb-4">
<h5>Popular Tags</h5>
{% for tag in popular_tags %}
<a href="#" class="tag">{{ tag.name }}</a>
{% endfor %}
</div>
{% endblock %}
</aside>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>3. Create Home Page Template
<!-- blog/templates/blog/home.html -->
{% extends 'blog/base.html' %}
{% block title %}Home - {{ block.super }}{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-12">
<h1 class="mb-4">Welcome to My Blog</h1>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Latest Articles</h3>
{% for article in latest_articles %}
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">
<a href="{{ article.get_absolute_url }}" class="text-decoration-none">
{{ article.title }}
</a>
</h5>
<p class="article-meta">
{{ article.author.username }} · {{ article.publish_date|date:"M d" }}
{% if article.category %}
· {{ article.category.name }}
{% endif %}
</p>
<p class="card-text">{{ article.excerpt }}</p>
</div>
</div>
{% empty %}
<p class="text-muted">No articles published yet.</p>
{% endfor %}
</div>
<div class="col-md-6">
<h3>Popular Articles</h3>
{% for article in popular_articles %}
<div class="card mb-3">
<div class="card-body">
<h6 class="card-title">
<a href="{{ article.get_absolute_url }}" class="text-decoration-none">
{{ article.title }}
</a>
</h6>
<p class="article-meta">
Views {{ article.view_count }} · {{ article.publish_date|date:"M d" }}
</p>
</div>
</div>
{% empty %}
<p class="text-muted">No popular articles yet.</p>
{% endfor %}
</div>
</div>
</div>
{% endblock %}4. Set URL Routing
Include application URLs in project's main URL configuration:
# mysite/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls')), # Include blog application URLs
]5. Execute Database Migration
# Create migration files
python manage.py makemigrations blog
# Execute migration
python manage.py migrate
# Create superuser
python manage.py createsuperuser6. Test Application
# Start development server
python manage.py runserver
# Access these URLs to test:
# http://127.0.0.1:8000/ - Home page
# http://127.0.0.1:8000/admin/ - Admin backend
# http://127.0.0.1:8000/articles/ - Article listApplication Best Practices
1. Single Responsibility Applications
Each application should focus on one specific function:
# Good application division
blog/ # Blog functionality
users/ # User management
comments/ # Comment system
api/ # API interface
# Avoid application division
main/ # Too broad
utils/ # Utility classes are not business functions2. Application Decoupling
Use signals, middleware, or service layers to handle interactions between applications:
# blog/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Article
@receiver(post_save, sender=Article)
def article_published(sender, instance, created, **kwargs):
"""Signal handling when article is published"""
if instance.status == 'published':
# Send notifications, update cache, etc.
pass3. Reusability Design
Design applications with reusability in mind:
# Use configuration to make applications more flexible
from django.conf import settings
BLOG_PAGINATION = getattr(settings, 'BLOG_PAGINATION', 10)
BLOG_EXCERPT_LENGTH = getattr(settings, 'BLOG_EXCERPT_LENGTH', 200)Summary
Django applications are important for organizing code:
- ✅ Use
python manage.py startappto create applications - ✅ Understand application directory structure and file functions
- ✅ Register applications in
settings.py - ✅ Follow single responsibility principle in application design
- ✅ Maintain low coupling between applications
Through reasonable application division, Django projects can be more modular, maintainable, and extensible.
Next Article
We will learn Django's URL routing system in depth.
3.1 Django URL Configuration Basics →