9、Python Web开发进阶 - Django框架

68 阅读12分钟

Python Web开发进阶 - Django框架

1. Django简介

作为Java开发者,您可能熟悉Spring这样的全栈框架。在Python世界中,Django是最流行的全栈Web框架,提供了丰富的功能和工具,遵循"batteries-included"(内置电池)的理念。

Django框架

Django MVT架构

Django遵循MVT(Model-View-Template)架构模式,这是MVC模式的一种变体:

graph TD
    A[浏览器] -->|HTTP请求| B[URLs路由]
    B -->|分发请求| C[视图View]
    C -->|查询/更新| D[模型Model]
    D -->|数据库操作| E[(数据库)]
    E -->|返回数据| D
    D -->|返回数据| C
    C -->|渲染| F[模板Template]
    F -->|HTML| C
    C -->|HTTP响应| A
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style C fill:#dfd,stroke:#333,stroke-width:2px
    style D fill:#fdd,stroke:#333,stroke-width:2px
    style E fill:#ddd,stroke:#333,stroke-width:2px
    style F fill:#ffd,stroke:#333,stroke-width:2px

Django的特点

  • 全栈框架:提供Web开发所需的几乎所有组件
  • 内置ORM:强大的数据库抽象层,无需编写SQL
  • 自动管理后台:自动生成数据管理界面
  • 表单处理:简化表单验证和处理
  • 认证系统:内置用户认证和权限管理
  • 缓存框架:灵活的缓存支持
  • 安全特性:内置防御常见Web攻击的机制

Django vs Flask

特性DjangoFlask
类型全栈框架微框架
学习曲线较陡平缓
灵活性中等
内置功能丰富
适用场景大型复杂应用小型到中型应用,API服务
项目结构相对固定自由定义
开发速度快(对于大型项目)快(对于小型项目)

2. 安装Django

使用pip安装Django:

# 创建虚拟环境
python -m venv venv

# 激活虚拟环境
# Windows
venv\Scripts\activate
# macOS/Linux
source venv/bin/activate

# 安装Django
pip install django

验证安装:

python -m django --version

3. 创建第一个Django项目

Django提供了命令行工具来创建项目和应用:

# 创建项目
django-admin startproject mysite

# 项目结构
mysite/
    manage.py            # 命令行工具
    mysite/              # 项目包
        __init__.py
        settings.py      # 项目设置
        urls.py          # URL配置
        asgi.py          # ASGI配置(异步服务器网关接口)
        wsgi.py          # WSGI配置(Web服务器网关接口)

运行开发服务器

cd mysite
python manage.py runserver

访问 http://127.0.0.1:8000/ 即可看到Django欢迎页面。

4. Django项目结构

创建应用

Django项目由多个应用组成,每个应用是一个Python包,实现特定功能:

python manage.py startapp blog

# 应用结构
blog/
    __init__.py
    admin.py          # 管理后台配置
    apps.py           # 应用配置
    migrations/       # 数据库迁移文件
        __init__.py
    models.py         # 数据模型
    tests.py          # 测试
    views.py          # 视图函数

注册应用

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',
    'blog',  # 添加我们的应用
]

5. 模型(Models)

Django的ORM允许通过Python类定义数据库模型:

# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone

class Category(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)
    
    def __str__(self):
        return self.name
    
    class Meta:
        verbose_name_plural = "Categories"

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    pub_date = models.DateTimeField(default=timezone.now)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
    is_published = models.BooleanField(default=True)
    
    def __str__(self):
        return self.title
    
    class Meta:
        ordering = ['-pub_date']  # 按发布日期降序排序

字段类型

Django提供了多种字段类型:

  • CharField:字符串字段,需要指定max_length
  • TextField:长文本字段
  • IntegerField:整数字段
  • FloatField:浮点数字段
  • BooleanField:布尔字段
  • DateField:日期字段
  • DateTimeField:日期时间字段
  • EmailField:电子邮件字段
  • FileField:文件上传字段
  • ImageField:图像上传字段
  • ForeignKey:外键关系
  • ManyToManyField:多对多关系
  • OneToOneField:一对一关系

字段选项

常用的字段选项:

  • null:如果为True,Django将在数据库中将空值存储为NULL
  • blank:如果为True,字段允许为空
  • default:字段的默认值
  • choices:一个二元组的可迭代对象,用于限制字段的选择
  • primary_key:如果为True,将该字段设为主键
  • unique:如果为True,该字段在表中必须是唯一的
  • verbose_name:字段的人类可读名称
  • auto_now:每次保存对象时自动设置为当前日期时间
  • auto_now_add:创建对象时自动设置为当前日期时间

数据库迁移

定义模型后,需要创建数据库迁移并应用:

# 创建迁移
python manage.py makemigrations

# 应用迁移
python manage.py migrate

模型查询

Django ORM提供了强大的查询API:

# 获取所有文章
posts = Post.objects.all()

# 获取单个文章
post = Post.objects.get(id=1)

# 过滤文章
published_posts = Post.objects.filter(is_published=True)
recent_posts = Post.objects.filter(pub_date__gte=timezone.now() - timezone.timedelta(days=7))

# 排序
ordered_posts = Post.objects.order_by('-pub_date')

# 链式查询
filtered_ordered_posts = Post.objects.filter(is_published=True).order_by('-pub_date')

# 限制结果数量
latest_posts = Post.objects.order_by('-pub_date')[:5]

# 复杂查询
from django.db.models import Q
complex_query = Post.objects.filter(Q(title__contains='Django') | Q(content__contains='Django'))

# 关联查询
user_posts = Post.objects.filter(author__username='john')
category_posts = Post.objects.filter(category__name='Technology')

6. 视图(Views)

Django的视图处理Web请求并返回响应:

基于函数的视图

# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse
from .models import Post, Category

def index(request):
    latest_posts = Post.objects.filter(is_published=True)[:5]
    return render(request, 'blog/index.html', {'latest_posts': latest_posts})

def post_detail(request, post_id):
    post = get_object_or_404(Post, id=post_id, is_published=True)
    return render(request, 'blog/post_detail.html', {'post': post})

def category_posts(request, category_id):
    category = get_object_or_404(Category, id=category_id)
    posts = Post.objects.filter(category=category, is_published=True)
    return render(request, 'blog/category_posts.html', {
        'category': category,
        'posts': posts
    })

基于类的视图

# blog/views.py
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from .models import Post

class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    queryset = Post.objects.filter(is_published=True)
    paginate_by = 10

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    template_name = 'blog/post_form.html'
    fields = ['title', 'content', 'category', 'is_published']
    success_url = reverse_lazy('blog:post_list')
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

class PostUpdateView(LoginRequiredMixin, UpdateView):
    model = Post
    template_name = 'blog/post_form.html'
    fields = ['title', 'content', 'category', 'is_published']
    
    def get_success_url(self):
        return reverse_lazy('blog:post_detail', kwargs={'pk': self.object.pk})

class PostDeleteView(LoginRequiredMixin, DeleteView):
    model = Post
    template_name = 'blog/post_confirm_delete.html'
    success_url = reverse_lazy('blog:post_list')

7. URL配置

Django使用URL配置将URL模式映射到视图:

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

app_name = 'blog'  # 命名空间

urlpatterns = [
    # 基于函数的视图
    path('', views.index, name='index'),
    path('post/<int:post_id>/', views.post_detail, name='post_detail'),
    path('category/<int:category_id>/', views.category_posts, name='category_posts'),
    
    # 基于类的视图
    path('posts/', views.PostListView.as_view(), name='post_list'),
    path('posts/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),
    path('posts/new/', views.PostCreateView.as_view(), name='post_create'),
    path('posts/<int:pk>/edit/', views.PostUpdateView.as_view(), name='post_update'),
    path('posts/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
]

在项目的urls.py中包含应用的URL配置:

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
]

8. 模板(Templates)

Django使用自己的模板语言,类似于Jinja2:

模板目录结构

mysite/
    blog/
        templates/
            blog/
                base.html
                index.html
                post_detail.html
                post_list.html
                post_form.html
                post_confirm_delete.html
                category_posts.html

基础模板

<!-- blog/templates/blog/base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Django Blog{% endblock %}</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    {% load static %}
    <link rel="stylesheet" href="{% static 'blog/style.css' %}">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="{% url 'blog:index' %}">Django Blog</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav ml-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'blog:index' %}">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'blog:post_list' %}">All Posts</a>
                    </li>
                    {% if user.is_authenticated %}
                        <li class="nav-item">
                            <a class="nav-link" href="{% url 'blog:post_create' %}">New Post</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{% url 'logout' %}">Logout</a>
                        </li>
                    {% else %}
                        <li class="nav-item">
                            <a class="nav-link" href="{% url 'login' %}">Login</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{% url 'register' %}">Register</a>
                        </li>
                    {% endif %}
                </ul>
            </div>
        </div>
    </nav>

    <div class="container mt-4">
        {% if messages %}
            {% for message in messages %}
                <div class="alert alert-{{ message.tags }}">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}

        {% block content %}{% endblock %}
    </div>

    <footer class="bg-dark text-white text-center py-3 mt-5">
        <div class="container">
            <p>&copy; 2025 Django Blog</p>
        </div>
    </footer>

    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>

列表模板

<!-- blog/templates/blog/post_list.html -->
{% extends 'blog/base.html' %}

{% block title %}All Posts - Django Blog{% endblock %}

{% block content %}
    <h1>All Posts</h1>
    
    {% if posts %}
        <div class="row">
            {% for post in posts %}
                <div class="col-md-6 mb-4">
                    <div class="card">
                        <div class="card-body">
                            <h5 class="card-title">{{ post.title }}</h5>
                            <h6 class="card-subtitle mb-2 text-muted">
                                By {{ post.author.username }} on {{ post.pub_date|date:"F d, Y" }}
                            </h6>
                            {% if post.category %}
                                <span class="badge badge-secondary">{{ post.category.name }}</span>
                            {% endif %}
                            <p class="card-text">{{ post.content|truncatewords:30 }}</p>
                            <a href="{% url 'blog:post_detail' post.id %}" class="card-link">Read more</a>
                        </div>
                    </div>
                </div>
            {% endfor %}
        </div>
        
        {% if is_paginated %}
            <nav>
                <ul class="pagination">
                    {% if page_obj.has_previous %}
                        <li class="page-item">
                            <a class="page-link" href="?page=1">&laquo; First</a>
                        </li>
                        <li class="page-item">
                            <a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
                        </li>
                    {% endif %}
                    
                    {% for num in page_obj.paginator.page_range %}
                        {% if page_obj.number == num %}
                            <li class="page-item active">
                                <span class="page-link">{{ num }}</span>
                            </li>
                        {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
                            <li class="page-item">
                                <a class="page-link" href="?page={{ num }}">{{ num }}</a>
                            </li>
                        {% endif %}
                    {% endfor %}
                    
                    {% if page_obj.has_next %}
                        <li class="page-item">
                            <a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
                        </li>
                        <li class="page-item">
                            <a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last &raquo;</a>
                        </li>
                    {% endif %}
                </ul>
            </nav>
        {% endif %}
    {% else %}
        <p>No posts available.</p>
    {% endif %}
{% endblock %}

详情模板

<!-- blog/templates/blog/post_detail.html -->
{% extends 'blog/base.html' %}

{% block title %}{{ post.title }} - Django Blog{% endblock %}

{% block content %}
    <article>
        <h1>{{ post.title }}</h1>
        <p class="text-muted">
            By {{ post.author.username }} on {{ post.pub_date|date:"F d, Y" }}
            {% if post.category %}
                in <a href="{% url 'blog:category_posts' post.category.id %}">{{ post.category.name }}</a>
            {% endif %}
        </p>
        
        <div class="content">
            {{ post.content|linebreaks }}
        </div>
        
        {% if user == post.author %}
            <div class="mt-4">
                <a href="{% url 'blog:post_update' post.id %}" class="btn btn-primary">Edit</a>
                <a href="{% url 'blog:post_delete' post.id %}" class="btn btn-danger">Delete</a>
            </div>
        {% endif %}
    </article>
{% endblock %}

9. 表单(Forms)

Django提供了强大的表单处理功能:

模型表单

# blog/forms.py
from django import forms
from .models import Post, Category

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content', 'category', 'is_published']
        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
            'category': forms.Select(attrs={'class': 'form-control'}),
            'is_published': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
        }

class CategoryForm(forms.ModelForm):
    class Meta:
        model = Category
        fields = ['name', 'description']
        widgets = {
            'name': forms.TextInput(attrs={'class': 'form-control'}),
            'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 5}),
        }

自定义表单

# blog/forms.py
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100, widget=forms.TextInput(attrs={'class': 'form-control'}))
    email = forms.EmailField(widget=forms.EmailInput(attrs={'class': 'form-control'}))
    subject = forms.CharField(max_length=200, widget=forms.TextInput(attrs={'class': 'form-control'}))
    message = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 5}))

在视图中使用表单

# blog/views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import ContactForm

def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            # 处理表单数据
            name = form.cleaned_data['name']
            email = form.cleaned_data['email']
            subject = form.cleaned_data['subject']
            message = form.cleaned_data['message']
            
            # 发送电子邮件或保存到数据库
            # ...
            
            messages.success(request, 'Your message has been sent!')
            return redirect('blog:contact')
    else:
        form = ContactForm()
    
    return render(request, 'blog/contact.html', {'form': form})

表单模板

<!-- blog/templates/blog/contact.html -->
{% extends 'blog/base.html' %}

{% block title %}Contact Us - Django Blog{% endblock %}

{% block content %}
    <h1>Contact Us</h1>
    
    <form method="post">
        {% csrf_token %}
        
        {% for field in form %}
            <div class="form-group">
                {{ field.label_tag }}
                {{ field }}
                {% if field.help_text %}
                    <small class="form-text text-muted">{{ field.help_text }}</small>
                {% endif %}
                {% if field.errors %}
                    <div class="invalid-feedback d-block">
                        {% for error in field.errors %}
                            {{ error }}
                        {% endfor %}
                    </div>
                {% endif %}
            </div>
        {% endfor %}
        
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
{% endblock %}

10. 管理后台(Admin)

Django自动生成管理后台界面:

注册模型

# blog/admin.py
from django.contrib import admin
from .models import Category, Post

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'description')
    search_fields = ('name',)

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'category', 'pub_date', 'is_published')
    list_filter = ('is_published', 'category', 'pub_date')
    search_fields = ('title', 'content')
    date_hierarchy = 'pub_date'
    list_editable = ('is_published',)
    readonly_fields = ('pub_date',)
    fieldsets = (
        (None, {
            'fields': ('title', 'content')
        }),
        ('Publishing options', {
            'fields': ('author', 'category', 'pub_date', 'is_published')
        }),
    )

创建超级用户

python manage.py createsuperuser

访问 http://127.0.0.1:8000/admin/ 登录管理后台。

11. 用户认证

Django提供了内置的用户认证系统:

用户注册

# accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class UserRegisterForm(UserCreationForm):
    email = forms.EmailField()
    
    class Meta:
        model = User
        fields = ['username', 'email', 'password1', 'password2']
# accounts/views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import UserRegisterForm

def register(request):
    if request.method == 'POST':
        form = UserRegisterForm(request.POST)
        if form.is_valid():
            form.save()
            username = form.cleaned_data.get('username')
            messages.success(request, f'Account created for {username}! You can now log in.')
            return redirect('login')
    else:
        form = UserRegisterForm()
    return render(request, 'accounts/register.html', {'form': form})

登录和登出

Django提供了内置的视图处理登录和登出:

# mysite/urls.py
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views
from accounts import views as accounts_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
    path('register/', accounts_views.register, name='register'),
    path('login/', auth_views.LoginView.as_view(template_name='accounts/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(template_name='accounts/logout.html'), name='logout'),
]

保护视图

使用装饰器保护基于函数的视图:

from django.contrib.auth.decorators import login_required

@login_required
def create_post(request):
    # 只有登录用户可以访问
    # ...

使用混入类保护基于类的视图:

from django.contrib.auth.mixins import LoginRequiredMixin

class PostCreateView(LoginRequiredMixin, CreateView):
    # 只有登录用户可以访问
    # ...

12. 静态文件

Django自动处理静态文件:

配置静态文件

# mysite/settings.py
STATIC_URL = '/static/'
STATICFILES_DIRS = [
    BASE_DIR / "static",
]
STATIC_ROOT = BASE_DIR / "staticfiles"

应用静态文件

mysite/
    blog/
        static/
            blog/
                css/
                    style.css
                js/
                    script.js
                images/
                    logo.png

在模板中使用静态文件

{% load static %}
<link rel="stylesheet" href="{% static 'blog/css/style.css' %}">
<script src="{% static 'blog/js/script.js' %}"></script>
<img src="{% static 'blog/images/logo.png' %}" alt="Logo">

收集静态文件

在生产环境中,需要收集所有静态文件到一个目录:

python manage.py collectstatic

13. 媒体文件

Django可以处理用户上传的媒体文件:

配置媒体文件

# mysite/settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

在开发环境中提供媒体文件

# mysite/urls.py
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # ...
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

添加图片字段到模型

# blog/models.py
class Post(models.Model):
    # ...
    image = models.ImageField(upload_to='post_images/', blank=True, null=True)
    # ...

注意:需要安装Pillow库来处理图像:

pip install Pillow

14. 中间件

中间件是处理请求和响应的钩子:

内置中间件

Django包含多个内置中间件:

# mysite/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

自定义中间件

# blog/middleware.py
import time

class RequestTimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        # 请求处理前的代码
        start_time = time.time()
        
        # 处理请求
        response = self.get_response(request)
        
        # 请求处理后的代码
        duration = time.time() - start_time
        print(f"Request to {request.path} took {duration:.2f}s")
        
        return response

注册中间件:

# mysite/settings.py
MIDDLEWARE = [
    # ...
    'blog.middleware.RequestTimingMiddleware',
]

15. 测试

Django提供了测试框架:

模型测试

# blog/tests.py
from django.test import TestCase
from django.contrib.auth.models import User
from .models import Category, Post

class PostModelTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        # 创建测试数据
        test_user = User.objects.create_user(username='testuser', password='12345')
        test_category = Category.objects.create(name='Test Category')
        Post.objects.create(
            title='Test Post',
            content='This is a test post content.',
            author=test_user,
            category=test_category
        )
    
    def test_title_content(self):
        post = Post.objects.get(id=1)
        expected_title = 'Test Post'
        self.assertEqual(post.title, expected_title)
    
    def test_post_str(self):
        post = Post.objects.get(id=1)
        self.assertEqual(str(post), post.title)

视图测试

# blog/tests.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Post, Category

class PostViewsTest(TestCase):
    def setUp(self):
        # 创建测试客户端
        self.client = Client()
        
        # 创建测试用户
        self.user = User.objects.create_user(username='testuser', password='12345')
        
        # 创建测试数据
        self.category = Category.objects.create(name='Test Category')
        self.post = Post.objects.create(
            title='Test Post',
            content='This is a test post content.',
            author=self.user,
            category=self.category
        )
    
    def test_post_list_view(self):
        response = self.client.get(reverse('blog:post_list'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Test Post')
        self.assertTemplateUsed(response, 'blog/post_list.html')
    
    def test_post_detail_view(self):
        response = self.client.get(reverse('blog:post_detail', args=[self.post.id]))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Test Post')
        self.assertContains(response, 'This is a test post content.')
        self.assertTemplateUsed(response, 'blog/post_detail.html')
    
    def test_post_create_view_redirect_if_not_logged_in(self):
        response = self.client.get(reverse('blog:post_create'))
        self.assertRedirects(response, '/login/?next=/blog/posts/new/')
    
    def test_post_create_view_logged_in(self):
        self.client.login(username='testuser', password='12345')
        response = self.client.get(reverse('blog:post_create'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'blog/post_form.html')

运行测试

# 运行所有测试
python manage.py test

# 运行特定应用的测试
python manage.py test blog

# 运行特定测试类
python manage.py test blog.tests.PostModelTest

# 运行特定测试方法
python manage.py test blog.tests.PostModelTest.test_title_content

16. 部署Django应用

生产环境设置

在生产环境中,需要修改settings.py

# mysite/settings.py
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

# 安全设置
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True

使用Gunicorn和Nginx

与Flask类似,Django应用通常使用Gunicorn作为WSGI服务器,Nginx作为反向代理:

# 安装Gunicorn
pip install gunicorn

# 运行应用
gunicorn mysite.wsgi:application

使用Docker部署

创建Dockerfile

FROM python:3.9

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN python manage.py collectstatic --noinput

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "mysite.wsgi:application"]

创建docker-compose.yml

version: '3'

services:
  db:
    image: postgres:13
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - ./.env
  
  web:
    build: .
    command: gunicorn mysite.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - .:/app
      - static_volume:/app/staticfiles
      - media_volume:/app/media
    depends_on:
      - db
    env_file:
      - ./.env
  
  nginx:
    image: nginx:1.19
    ports:
      - "80:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - static_volume:/home/app/staticfiles
      - media_volume:/home/app/media
    depends_on:
      - web

volumes:
  postgres_data:
  static_volume:
  media_volume:

17. Django REST Framework

Django REST Framework (DRF) 是一个强大的工具包,用于构建Web API:

安装DRF

pip install djangorestframework

settings.py中注册:

INSTALLED_APPS = [
    # ...
    'rest_framework',
]

序列化器

# blog/serializers.py
from rest_framework import serializers
from .models import Post, Category
from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email']

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ['id', 'name', 'description']

class PostSerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)
    category = CategorySerializer(read_only=True)
    category_id = serializers.PrimaryKeyRelatedField(
        queryset=Category.objects.all(),
        source='category',
        write_only=True,
        required=False
    )
    
    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'pub_date', 'author', 'category', 'category_id', 'is_published']
        read_only_fields = ['pub_date']

API视图

# blog/api_views.py
from rest_framework import viewsets, permissions
from .models import Post, Category
from .serializers import PostSerializer, CategorySerializer
from .permissions import IsAuthorOrReadOnly

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

class CategoryViewSet(viewsets.ModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

自定义权限

# blog/permissions.py
from rest_framework import permissions

class IsAuthorOrReadOnly(permissions.BasePermission):
    """
    自定义权限:只允许对象的作者编辑它
    """
    def has_object_permission(self, request, view, obj):
        # 读取权限允许任何请求
        if request.method in permissions.SAFE_METHODS:
            return True
        
        # 写入权限只允许对象的作者
        return obj.author == request.user

API URL配置

# blog/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import api_views

router = DefaultRouter()
router.register(r'posts', api_views.PostViewSet)
router.register(r'categories', api_views.CategoryViewSet)

urlpatterns = [
    # ...
    path('api/', include(router.urls)),
]

18. 练习:构建完整的博客应用

让我们整合所学知识,构建一个完整的博客应用:

项目结构

blog_project/
    manage.py
    blog_project/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py
    blog/
        __init__.py
        admin.py
        apps.py
        forms.py
        models.py
        permissions.py
        serializers.py
        tests.py
        urls.py
        views.py
        api_views.py
        migrations/
        templates/
            blog/
                base.html
                index.html
                post_list.html
                post_detail.html
                post_form.html
                post_confirm_delete.html
        static/
            blog/
                css/
                    style.css
                js/
                    script.js
    accounts/
        __init__.py
        admin.py
        apps.py
        forms.py
        models.py
        tests.py
        urls.py
        views.py
        templates/
            accounts/
                register.html
                login.html
                logout.html
                profile.html

功能列表

  1. 用户认证(注册、登录、登出)
  2. 用户个人资料
  3. 博客文章的CRUD操作
  4. 分类管理
  5. 评论系统
  6. 搜索功能
  7. REST API

实现步骤

  1. 创建项目和应用
  2. 定义模型
  3. 创建视图和模板
  4. 实现表单处理
  5. 配置URL
  6. 添加用户认证
  7. 实现REST API
  8. 编写测试
  9. 部署应用

19. 今日总结

  • Django是一个全栈Web框架,提供了丰富的内置功能
  • Django遵循MTV(模型-模板-视图)架构模式
  • Django的ORM提供了强大的数据库抽象层
  • Django的管理后台可以自动生成数据管理界面
  • Django提供了内置的用户认证系统
  • Django REST Framework可以轻松构建Web API
  • Django的测试框架支持单元测试和集成测试

20. 明日预告

明天我们将学习Python数据分析与可视化,包括NumPy、pandas和Matplotlib等库的使用。这些库是Python数据科学生态系统的核心组件,可以帮助您处理、分析和可视化数据。