我们继续第29天的学习,主题是Python Web开发。我们将学习使用Flask和Django框架来构建Web应用程序。
由于内容较多,我们将分为两部分:Flask轻量级框架和Django全功能框架。
今天的学习目标:
- 学习Flask框架,构建一个简单的Web应用
- 学习Django框架,创建一个完整的项目
我们将通过实例来学习,包括路由、模板、表单、数据库等。
第一部分:Flask轻量级Web框架
1.1 Flask基础和环境搭建
第一安装必要的库:
pip install flask flask-sqlalchemy flask-wtf flask-login flask-bootstrap
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, TextAreaField, SelectField, DecimalField
from wtforms.validators import DataRequired, Email, Length
from flask_bootstrap import Bootstrap
from datetime import datetime
import os
from werkzeug.security import generate_password_hash, check_password_hash
# 初始化Flask应用
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# 初始化扩展
db = SQLAlchemy(app)
Bootstrap(app)
# 数据模型
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
posts = db.relationship('Post', backref='author', lazy=True)
comments = db.relationship('Comment', backref='author', lazy=True)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
category = db.Column(db.String(50), default='General')
comments = db.relationship('Comment', backref='post', lazy=True, cascade='all, delete-orphan')
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
# 表单类
class RegistrationForm(FlaskForm):
username = StringField('用户名', validators=[DataRequired(), Length(min=4, max=80)])
email = StringField('邮箱', validators=[DataRequired(), Email()])
password = PasswordField('密码', validators=[DataRequired(), Length(min=6)])
class LoginForm(FlaskForm):
username = StringField('用户名', validators=[DataRequired()])
password = PasswordField('密码', validators=[DataRequired()])
class PostForm(FlaskForm):
title = StringField('标题', validators=[DataRequired(), Length(max=200)])
content = TextAreaField('内容', validators=[DataRequired()])
category = SelectField('分类', choices=[
('General', '通用'),
('Technology', '技术'),
('Life', '生活'),
('Travel', '旅行'),
('Food', '美食')
])
class CommentForm(FlaskForm):
content = TextAreaField('评论', validators=[DataRequired()])
# 创建数据库表
with app.app_context():
db.create_all()
# 路由和视图函数
@app.route('/')
def index():
"""首页"""
page = request.args.get('page', 1, type=int)
posts = Post.query.order_by(Post.created_at.desc()).paginate(
page=page, per_page=5, error_out=False
)
return render_template('index.html', posts=posts)
@app.route('/about')
def about():
"""关于页面"""
return render_template('about.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
"""用户注册"""
form = RegistrationForm()
if form.validate_on_submit():
# 检查用户名和邮箱是否已存在
existing_user = User.query.filter(
(User.username == form.username.data) |
(User.email == form.email.data)
).first()
if existing_user:
flash('用户名或邮箱已存在', 'danger')
return render_template('register.html', form=form)
# 创建新用户
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('注册成功!请登录。', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
@app.route('/login', methods=['GET', 'POST'])
def login():
"""用户登录"""
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data):
session['user_id'] = user.id
session['username'] = user.username
flash(f'欢迎回来,{user.username}!', 'success')
return redirect(url_for('index'))
else:
flash('用户名或密码错误', 'danger')
return render_template('login.html', form=form)
@app.route('/logout')
def logout():
"""用户登出"""
session.clear()
flash('您已成功登出', 'info')
return redirect(url_for('index'))
@app.route('/post/new', methods=['GET', 'POST'])
def new_post():
"""创建新文章"""
if 'user_id' not in session:
flash('请先登录', 'warning')
return redirect(url_for('login'))
form = PostForm()
if form.validate_on_submit():
post = Post(
title=form.title.data,
content=form.content.data,
category=form.category.data,
user_id=session['user_id']
)
db.session.add(post)
db.session.commit()
flash('文章发布成功!', 'success')
return redirect(url_for('index'))
return render_template('create_post.html', form=form)
@app.route('/post/<int:post_id>')
def post_detail(post_id):
"""文章详情"""
post = Post.query.get_or_404(post_id)
form = CommentForm()
return render_template('post_detail.html', post=post, form=form)
@app.route('/post/<int:post_id>/comment', methods=['POST'])
def add_comment(post_id):
"""添加评论"""
if 'user_id' not in session:
flash('请先登录', 'warning')
return redirect(url_for('login'))
form = CommentForm()
if form.validate_on_submit():
comment = Comment(
content=form.content.data,
user_id=session['user_id'],
post_id=post_id
)
db.session.add(comment)
db.session.commit()
flash('评论发布成功!', 'success')
return redirect(url_for('post_detail', post_id=post_id))
@app.route('/profile')
def profile():
"""用户个人资料"""
if 'user_id' not in session:
flash('请先登录', 'warning')
return redirect(url_for('login'))
user = User.query.get(session['user_id'])
user_posts = Post.query.filter_by(user_id=user.id).order_by(Post.created_at.desc()).all()
return render_template('profile.html', user=user, posts=user_posts)
@app.route('/api/posts')
def api_posts():
"""API接口:获取文章列表"""
posts = Post.query.order_by(Post.created_at.desc()).limit(10).all()
posts_data = []
for post in posts:
posts_data.append({
'id': post.id,
'title': post.title,
'content': post.content[:100] + '...' if len(post.content) > 100 else post.content,
'author': post.author.username,
'created_at': post.created_at.isoformat(),
'category': post.category
})
return jsonify(posts_data)
# 错误处理
@app.errorhandler(404)
def not_found_error(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('500.html'), 500
if __name__ == '__main__':
app.run(debug=True)
1.2 Flask模板文件
创建templates文件夹,并添加以下模板文件:
base.html (基础模板)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}我的博客{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.navbar-brand { font-weight: bold; }
.post-card { transition: transform 0.2s; }
.post-card:hover { transform: translateY(-2px); }
.category-badge { font-size: 0.8em; }
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">
<i class="fas fa-blog"></i> 我的博客
</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_for('index') }}">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('about') }}">关于</a>
</li>
</ul>
<ul class="navbar-nav">
{% if session.username %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('new_post') }}">
<i class="fas fa-plus"></i> 写文章
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user"></i> {{ session.username }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('profile') }}">个人资料</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('logout') }}">退出</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">登录</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('register') }}">注册</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- 主要内容 -->
<div class="container mt-4">
<!-- 闪存消息 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<!-- 页脚 -->
<footer class="bg-dark text-light mt-5 py-4">
<div class="container text-center">
<p>© 2024 我的博客. 使用 Flask 构建.</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
index.html (首页)
{% extends "base.html" %}
{% block title %}首页 - 我的博客{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8">
<h2 class="mb-4">最新文章</h2>
{% for post in posts.items %}
<div class="card post-card mb-4 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="card-title">
<a href="{{ url_for('post_detail', post_id=post.id) }}" class="text-decoration-none">
{{ post.title }}
</a>
</h5>
<span class="badge bg-primary category-badge">{{ post.category }}</span>
</div>
<p class="card-text text-muted">
{{ post.content|striptags|truncate(200) }}
</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<i class="fas fa-user"></i> {{ post.author.username }}
<i class="fas fa-clock ms-2"></i> {{ post.created_at.strftime('%Y-%m-%d %H:%M') }}
<i class="fas fa-comment ms-2"></i> {{ post.comments|length }} 评论
</small>
<a href="{{ url_for('post_detail', post_id=post.id) }}" class="btn btn-sm btn-outline-primary">
阅读更多
</a>
</div>
</div>
</div>
{% else %}
<div class="alert alert-info">
还没有文章,<a href="{{ url_for('new_post') }}" class="alert-link">点击这里</a> 创建第一篇吧!
</div>
{% endfor %}
<!-- 分页 -->
{% if posts.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if posts.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('index', page=posts.prev_num) }}">上一页</a>
</li>
{% endif %}
{% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if page_num %}
<li class="page-item {% if page_num == posts.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('index', page=page_num) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">…</span></li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('index', page=posts.next_num) }}">下一页</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
<div class="col-lg-4">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="card-title">关于博客</h5>
<p class="card-text">这是一个使用 Flask 构建的个人博客系统,支持用户注册、发布文章、评论等功能。</p>
<a href="{{ url_for('about') }}" class="btn btn-outline-primary btn-sm">了解更多</a>
</div>
</div>
<div class="card mt-4 shadow-sm">
<div class="card-body">
<h5 class="card-title">分类统计</h5>
<ul class="list-unstyled">
{% set categories = ['General', 'Technology', 'Life', 'Travel', 'Food'] %}
{% for category in categories %}
<li>
<a href="#" class="text-decoration-none">
{{ category }} <span class="badge bg-secondary float-end">0</span>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
register.html (注册页面)
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}注册 - 我的博客{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-body">
<h2 class="card-title text-center mb-4">用户注册</h2>
{{ wtf.quick_form(form, button_map={'submit': 'primary'}) }}
<hr>
<p class="text-center">
已有账号? <a href="{{ url_for('login') }}">点击登录</a>
</p>
</div>
</div>
</div>
</div>
{% endblock %}
继续创建其他模板文件…
第二部分:Django全功能Web框架
2.1 Django项目创建和配置
第一安装Django:
pip install django django-crispy-forms pillow
然后创建Django项目:
django-admin startproject myblog
cd myblog
python manage.py startapp blog
python manage.py startapp users
myblog/settings.py 配置:
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = 'your-secret-key-here'
DEBUG = True
ALLOWED_HOSTS = []
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'crispy_forms',
'crispy_bootstrap5',
'blog',
'users',
]
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',
]
ROOT_URLCONF = 'myblog.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'myblog.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_TZ = True
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
LOGIN_REDIRECT_URL = 'blog:home'
LOGIN_URL = 'users:login'
LOGOUT_REDIRECT_URL = 'blog:home'
2.2 Django数据模型
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):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = '分类'
verbose_name_plural = '分类'
ordering = ['name']
def __str__(self):
return self.name
class Post(models.Model):
STATUS_CHOICES = [
('draft', '草稿'),
('published', '已发布'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique_for_date='created_at')
content = models.TextField()
excerpt = models.TextField(max_length=500, blank=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='posts')
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
featured_image = models.ImageField(upload_to='blog_images/', blank=True, null=True)
# 时间字段
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)
# 统计字段
view_count = models.PositiveIntegerField(default=0)
like_count = models.PositiveIntegerField(default=0)
class Meta:
verbose_name = '文章'
verbose_name_plural = '文章'
ordering = ['-created_at']
indexes = [
models.Index(fields=['-created_at']),
models.Index(fields=['status']),
]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:post_detail', args=[
self.created_at.year,
self.created_at.month,
self.created_at.day,
self.slug
])
def save(self, *args, **kwargs):
if self.status == 'published' and not self.published_at:
self.published_at = timezone.now()
super().save(*args, **kwargs)
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_comments')
content = models.TextField()
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='replies')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True)
class Meta:
verbose_name = '评论'
verbose_name_plural = '评论'
ordering = ['created_at']
def __str__(self):
return f'评论 by {self.author} on {self.post}'
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
posts = models.ManyToManyField(Post, related_name='tags', blank=True)
class Meta:
verbose_name = '标签'
verbose_name_plural = '标签'
ordering = ['name']
def __str__(self):
return self.name
class Like(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['user', 'post']
verbose_name = '点赞'
verbose_name_plural = '点赞'
def __str__(self):
return f'{self.user} 喜爱 {self.post}'
2.3 Django表单和视图
blog/forms.py:
from django import forms
from .models import Post, Comment, Category
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'excerpt', 'category', 'status', 'featured_image']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '输入文章标题'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10, 'placeholder': '输入文章内容'}),
'excerpt': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': '文章摘要(可选)'}),
'category': forms.Select(attrs={'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-control'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['category'].queryset = Category.objects.all()
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['content']
widgets = {
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': '写下你的评论...'
})
}
class SearchForm(forms.Form):
query = forms.CharField(
max_length=100,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '搜索文章...'
})
)
blog/views.py:
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.core.paginator import Paginator
from django.db.models import Q, Count
from django.http import JsonResponse
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.urls import reverse_lazy
from .models import Post, Comment, Category, Tag, Like
from .forms import PostForm, CommentForm, SearchForm
class PostListView(ListView):
model = Post
template_name = 'blog/home.html'
context_object_name = 'posts'
paginate_by = 6
def get_queryset(self):
queryset = Post.objects.filter(status='published').select_related('author', 'category').prefetch_related('tags')
# 分类过滤
category_slug = self.kwargs.get('category_slug')
if category_slug:
category = get_object_or_404(Category, name=category_slug)
queryset = queryset.filter(category=category)
# 搜索功能
query = self.request.GET.get('q')
if query:
queryset = queryset.filter(
Q(title__icontains=query) |
Q(content__icontains=query) |
Q(excerpt__icontains=query) |
Q(tags__name__icontains=query)
).distinct()
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.annotate(post_count=Count('posts'))
context['recent_posts'] = Post.objects.filter(status='published')[:5]
context['search_form'] = SearchForm(self.request.GET or None)
return context
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
return Post.objects.filter(status='published').select_related('author', 'category').prefetch_related('tags', 'comments__author')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['comment_form'] = CommentForm()
context['comments'] = self.object.comments.filter(active=True, parent__isnull=True)
# 增加阅读量
self.object.view_count += 1
self.object.save(update_fields=['view_count'])
return context
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def form_valid(self, form):
form.instance.author = self.request.user
messages.success(self.request, '文章创建成功!')
return super().form_valid(form)
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def form_valid(self, form):
messages.success(self.request, '文章更新成功!')
return super().form_valid(form)
def test_func(self):
post = self.get_object()
return self.request.user == post.author
class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('blog:home')
def test_func(self):
post = self.get_object()
return self.request.user == post.author
def delete(self, request, *args, **kwargs):
messages.success(request, '文章删除成功!')
return super().delete(request, *args, **kwargs)
@login_required
def add_comment(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == 'POST':
form = CommentForm(request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.author = request.user
comment.save()
messages.success(request, '评论发布成功!')
return redirect('blog:post_detail', pk=pk)
@login_required
def like_post(request, pk):
post = get_object_or_404(Post, pk=pk)
like, created = Like.objects.get_or_create(user=request.user, post=post)
if not created:
like.delete()
post.like_count -= 1
liked = False
else:
post.like_count += 1
liked = True
post.save(update_fields=['like_count'])
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'liked': liked,
'like_count': post.like_count
})
return redirect('blog:post_detail', pk=pk)
def category_list(request):
categories = Category.objects.annotate(post_count=Count('posts'))
return render(request, 'blog/category_list.html', {'categories': categories})
def about(request):
return render(request, 'blog/about.html')
2.4 Django URL配置
myblog/urls.py:
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls', namespace='blog')),
path('users/', include('users.urls', namespace='users')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
blog/urls.py:
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.PostListView.as_view(), name='home'),
path('about/', views.about, name='about'),
path('categories/', views.category_list, name='category_list'),
path('category/<str:category_slug>/', views.PostListView.as_view(), name='posts_by_category'),
path('post/new/', views.PostCreateView.as_view(), name='post_create'),
path('post/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),
path('post/<int:pk>/update/', views.PostUpdateView.as_view(), name='post_update'),
path('post/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
path('post/<int:pk>/comment/', views.add_comment, name='add_comment'),
path('post/<int:pk>/like/', views.like_post, name='like_post'),
]
2.5 Django管理后台
blog/admin.py:
from django.contrib import admin
from .models import Category, Post, Comment, Tag, Like
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'created_at', 'post_count']
search_fields = ['name']
prepopulated_fields = {'slug': ['name']}
def post_count(self, obj):
return obj.posts.count()
post_count.short_description = '文章数量'
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'category', 'status', 'created_at', 'view_count', 'like_count']
list_filter = ['status', 'category', 'created_at', 'author']
search_fields = ['title', 'content']
prepopulated_fields = {'slug': ['title']}
raw_id_fields = ['author']
date_hierarchy = 'created_at'
ordering = ['-created_at']
fieldsets = (
('基本信息', {
'fields': ('title', 'slug', 'author', 'category', 'status')
}),
('内容', {
'fields': ('content', 'excerpt', 'featured_image')
}),
('统计信息', {
'fields': ('view_count', 'like_count'),
'classes': ('collapse',)
}),
('时间信息', {
'fields': ('created_at', 'updated_at', 'published_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ['created_at', 'updated_at']
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ['author', 'post', 'created_at', 'active']
list_filter = ['active', 'created_at']
search_fields = ['author__username', 'content']
actions = ['approve_comments', 'disapprove_comments']
def approve_comments(self, request, queryset):
queryset.update(active=True)
approve_comments.short_description = "批准选中的评论"
def disapprove_comments(self, request, queryset):
queryset.update(active=False)
disapprove_comments.short_description = "禁用选中的评论"
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ['name', 'post_count']
search_fields = ['name']
def post_count(self, obj):
return obj.posts.count()
post_count.short_description = '文章数量'
@admin.register(Like)
class LikeAdmin(admin.ModelAdmin):
list_display = ['user', 'post', 'created_at']
list_filter = ['created_at']
search_fields = ['user__username', 'post__title']
admin.site.site_header = '博客管理系统'
admin.site.site_title = '博客管理'
admin.site.index_title = '站点管理'
第三部分:用户认证系统
users/views.py:
from django.shortcuts import render, redirect
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.views.generic import CreateView, UpdateView
from django.urls import reverse_lazy
from .forms import UserRegisterForm, UserUpdateForm, ProfileUpdateForm
class RegisterView(CreateView):
form_class = UserRegisterForm
template_name = 'users/register.html'
success_url = reverse_lazy('blog:home')
def form_valid(self, form):
response = super().form_valid(form)
username = form.cleaned_data.get('username')
messages.success(self.request, f'账号 {username} 创建成功!请登录。')
return response
def login_view(request):
if request.method == 'POST':
form = AuthenticationForm(request, data=request.POST)
if form.is_valid():
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
user = authenticate(username=username, password=password)
if user is not None:
login(request, user)
messages.success(request, f'欢迎回来,{username}!')
next_page = request.GET.get('next')
return redirect(next_page) if next_page else redirect('blog:home')
else:
messages.error(request, '用户名或密码错误')
else:
messages.error(request, '用户名或密码错误')
form = AuthenticationForm()
return render(request, 'users/login.html', {'form': form})
def logout_view(request):
logout(request)
messages.info(request, '您已成功退出登录')
return redirect('blog:home')
@login_required
def profile(request):
if request.method == 'POST':
user_form = UserUpdateForm(request.POST, instance=request.user)
profile_form = ProfileUpdateForm(request.POST, request.FILES, instance=request.user.profile)
if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
messages.success(request, '个人信息更新成功!')
return redirect('users:profile')
else:
user_form = UserUpdateForm(instance=request.user)
profile_form = ProfileUpdateForm(instance=request.user.profile)
context = {
'user_form': user_form,
'profile_form': profile_form
}
return render(request, 'users/profile.html', context)
第四部分:REST API开发
4.1 使用Django REST Framework
安装DRF:
pip install djangorestframework django-cors-headers
blog/api/serializers.py:
from rest_framework import serializers
from ..models import Post, Comment, Category, User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'date_joined']
class CategorySerializer(serializers.ModelSerializer):
post_count = serializers.ReadOnlyField()
class Meta:
model = Category
fields = ['id', 'name', 'description', 'post_count']
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
class Meta:
model = Comment
fields = ['id', 'author', 'content', 'created_at']
class PostListSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
comment_count = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ['id', 'title', 'excerpt', 'author', 'category',
'created_at', 'view_count', 'like_count', 'comment_count']
def get_comment_count(self, obj):
return obj.comments.count()
class PostDetailSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'category',
'created_at', 'updated_at', 'view_count', 'like_count', 'comments']
class PostCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['title', 'content', 'excerpt', 'category', 'status']
def create(self, validated_data):
validated_data['author'] = self.context['request'].user
return super().create(validated_data)
blog/api/views.py:
from rest_framework import generics, permissions, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from django.db.models import Q
from ..models import Post, Comment, Category
from .serializers import (
PostListSerializer, PostDetailSerializer, PostCreateSerializer,
CommentSerializer, CategorySerializer
)
class PostListView(generics.ListAPIView):
serializer_class = PostListSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
queryset = Post.objects.filter(status='published').select_related('author', 'category')
# 搜索功能
search_query = self.request.query_params.get('search', None)
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(content__icontains=search_query)
)
# 分类过滤
category_id = self.request.query_params.get('category', None)
if category_id:
queryset = queryset.filter(category_id=category_id)
return queryset
class PostDetailView(generics.RetrieveAPIView):
queryset = Post.objects.filter(status='published')
serializer_class = PostDetailSerializer
permission_classes = [permissions.AllowAny]
class PostCreateView(generics.CreateAPIView):
queryset = Post.objects.all()
serializer_class = PostCreateSerializer
permission_classes = [permissions.IsAuthenticated]
class CategoryListView(generics.ListAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [permissions.AllowAny]
@api_view(['POST'])
@permission_classes([permissions.IsAuthenticated])
def add_comment(request, pk):
try:
post = Post.objects.get(pk=pk, status='published')
except Post.DoesNotExist:
return Response({'error': '文章不存在'}, status=status.HTTP_404_NOT_FOUND)
serializer = CommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(author=request.user, post=post)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([permissions.IsAuthenticated])
def like_post(request, pk):
try:
post = Post.objects.get(pk=pk, status='published')
except Post.DoesNotExist:
return Response({'error': '文章不存在'}, status=status.HTTP_404_NOT_FOUND)
liked = post.likes.filter(user=request.user).exists()
if liked:
post.likes.filter(user=request.user).delete()
post.like_count -= 1
liked = False
else:
post.likes.create(user=request.user)
post.like_count += 1
liked = True
post.save(update_fields=['like_count'])
return Response({
'liked': liked,
'like_count': post.like_count
})
第五部分:部署和优化
5.1 生产环境配置
myblog/settings_production.py:
from .settings import *
import os
DEBUG = False
ALLOWED_HOSTS = ['your-domain.com', 'www.your-domain.com']
# 数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USER'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOST'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
# 静态文件配置
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# 安全配置
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
# 日志配置
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.FileHandler',
'filename': os.path.join(BASE_DIR, 'logs/django.log'),
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': True,
},
},
}
5.2 Docker部署
Dockerfile:
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y
gcc
postgresql-dev
&& rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY requirements.txt .
# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制项目文件
COPY . .
# 收集静态文件
RUN python manage.py collectstatic --noinput
# 创建日志目录
RUN mkdir -p /app/logs
# 设置环境变量
ENV PYTHONUNBUFFERED=1
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myblog.wsgi:application"]
docker-compose.yml:
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DB_NAME=myblog
- DB_USER=postgres
- DB_PASSWORD=password
- DB_HOST=db
- SECRET_KEY=your-production-secret-key
depends_on:
- db
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/media
db:
image: postgres:13
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=myblog
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- static_volume:/app/staticfiles
- media_volume:/app/media
- ./ssl:/etc/nginx/ssl
depends_on:
- web
volumes:
postgres_data:
static_volume:
media_volume:
今日重点学习内容
- Flask框架:轻量级Web应用开发
- Django框架:全功能Web应用开发
- 数据库模型:ORM设计和关系管理
- 用户认证:注册、登录、权限控制
- REST API:使用Django REST Framewor
- 部署优化:生产环境配置和Docker部署
核心技能点
- MVC/MVT架构:理解Web框架的设计模式
- 表单处理:数据验证和用户输入处理
- 模板系统:动态HTML页面生成
- 路由配置:URL设计和视图映射
- 中间件:请求/响应处理流程
- 安全思考:CSRF防护、SQL注入防护等
扩展练习
- 添加更多功能:文件上传、邮件通知、全文搜索
- 性能优化:缓存策略、数据库查询优化
- 前端集成:使用Vue.js或React构建前
- 第三方集成:社交媒体登录、支付接口
明日预告
第30天:我们将进行项目实战,综合运用之前学过的所有知识,构建一个完整的全栈应用程序。
今天的内容超级丰富,提议先运行Flask示例,再逐步学习Django。有什么具体问题随时问我!
© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
您必须登录才能参与评论!
立即登录



收藏了,感谢分享