Skip to content

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.py

Create Application Command ​

python
python manage.py startapp <app_name>

Create Blog Application ​

bash
# 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 functions

Create Multiple Applications ​

Can only create one application at a time.

bash
# 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 api

Application Structure Details ​

__init__.py ​

Empty file, identifies this as a Python package.

apps.py ​

Application configuration file:

python
# 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.signals

models.py ​

python
# 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 ​

python
# 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)
  • 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 query

Usage 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 ​

python
# 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 ​

python
# 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:

python
# 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:

  1. Clarity: Clearly specifies which configuration class to use
  2. Customization: Can add custom configurations in BlogConfig class
  3. 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 ​

bash
# Create template directory in application directory
mkdir -p blog/templates/blog

2. Create Base Template ​

html
<!-- 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 ​

html
<!-- 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:

python
# 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 ​

bash
# Create migration files
python manage.py makemigrations blog

# Execute migration
python manage.py migrate

# Create superuser
python manage.py createsuperuser

6. Test Application ​

bash
# 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 list

Application Best Practices ​

1. Single Responsibility Applications ​

Each application should focus on one specific function:

bash
# 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 functions

2. Application Decoupling ​

Use signals, middleware, or service layers to handle interactions between applications:

python
# 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.
        pass

3. Reusability Design ​

Design applications with reusability in mind:

python
# 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:

  1. ✅ Use python manage.py startapp to create applications
  2. ✅ Understand application directory structure and file functions
  3. ✅ Register applications in settings.py
  4. ✅ Follow single responsibility principle in application design
  5. ✅ 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 →

Directory ​

Return to Course Directory

Released under the [BY-NC-ND License](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.en).