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