活动公告

系统通知
05-18 21:22
系统通知
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,资源失效请在帖子内回复要求补档,会尽快处理!
10-23 09:31

Django项目实战教程从环境搭建到成品输出的完整开发指南包含数据库设计模板创建视图函数配置和部署上线

SunJu_FaceMall

3万

主题

2860

科技点

3万

积分

白金月票

碾压王

积分
32872

塔罗立华奏

<font color=白金月票" /> 发表于 2025-9-24 16:20:16 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

Django是一个基于Python的高级Web框架,它鼓励快速开发和干净、实用的设计。本教程将带您从零开始,一步步构建一个完整的Django项目,涵盖环境搭建、数据库设计、模板创建、视图函数配置以及最终部署上线的全过程。无论您是Django新手还是有一定经验的开发者,本教程都将为您提供实用的指导和最佳实践。

1. 环境搭建

1.1 Python环境安装

在开始Django开发之前,首先需要确保您的系统上安装了Python。Django 3.2+版本需要Python 3.6或更高版本。
  1. # 检查Python版本
  2. python --version
  3. # 或
  4. python3 --version
复制代码

如果尚未安装Python,请访问Python官网下载并安装适合您操作系统的版本。

1.2 虚拟环境创建

使用虚拟环境可以隔离不同项目的依赖,避免包版本冲突。推荐使用venv模块创建虚拟环境:
  1. # 创建虚拟环境
  2. python -m venv myproject_env
  3. # 激活虚拟环境
  4. # Windows
  5. myproject_env\Scripts\activate
  6. # macOS/Linux
  7. source myproject_env/bin/activate
复制代码

1.3 Django安装

激活虚拟环境后,使用pip安装Django:
  1. # 安装Django
  2. pip install django
  3. # 验证Django安装
  4. python -m django --version
复制代码

1.4 开发工具选择

选择一个合适的IDE或文本编辑器可以提高开发效率。推荐使用:

• PyCharm(专业版或社区版)
• Visual Studio Code(安装Python和Django插件)
• Sublime Text

2. 项目创建与配置

2.1 创建Django项目

使用Django的命令行工具创建新项目:
  1. # 创建项目
  2. django-admin startproject myproject
  3. # 进入项目目录
  4. cd myproject
复制代码

2.2 项目结构解析

创建的项目结构如下:
  1. myproject/
  2.     manage.py
  3.     myproject/
  4.         __init__.py
  5.         settings.py
  6.         urls.py
  7.         asgi.py
  8.         wsgi.py
复制代码

• manage.py:Django的命令行工具,用于管理项目
• settings.py:项目的配置文件
• urls.py:项目的URL声明
• wsgi.py:WSGI兼容的Web服务器入口
• asgi.py:ASGI兼容的Web服务器入口

2.3 项目基本配置

编辑settings.py文件,进行基本配置:
  1. # settings.py
  2. import os
  3. from pathlib import Path
  4. # 构建路径
  5. BASE_DIR = Path(__file__).resolve().parent.parent
  6. # 安全密钥
  7. SECRET_KEY = 'django-insecure-your-secret-key-here'
  8. # 调试模式(生产环境应设为False)
  9. DEBUG = True
  10. # 允许的主机
  11. ALLOWED_HOSTS = []
  12. # 已安装的应用
  13. INSTALLED_APPS = [
  14.     'django.contrib.admin',
  15.     'django.contrib.auth',
  16.     'django.contrib.contenttypes',
  17.     'django.contrib.sessions',
  18.     'django.contrib.messages',
  19.     'django.contrib.staticfiles',
  20. ]
  21. # 中间件
  22. MIDDLEWARE = [
  23.     'django.middleware.security.SecurityMiddleware',
  24.     'django.contrib.sessions.middleware.SessionMiddleware',
  25.     'django.middleware.common.CommonMiddleware',
  26.     'django.middleware.csrf.CsrfViewMiddleware',
  27.     'django.contrib.auth.middleware.AuthenticationMiddleware',
  28.     'django.contrib.messages.middleware.MessageMiddleware',
  29.     'django.middleware.clickjacking.XFrameOptionsMiddleware',
  30. ]
  31. # 根URL配置
  32. ROOT_URLCONF = 'myproject.urls'
  33. # 模板配置
  34. TEMPLATES = [
  35.     {
  36.         'BACKEND': 'django.template.backends.django.DjangoTemplates',
  37.         'DIRS': [BASE_DIR / 'templates'],
  38.         'APP_DIRS': True,
  39.         'OPTIONS': {
  40.             'context_processors': [
  41.                 'django.template.context_processors.debug',
  42.                 'django.template.context_processors.request',
  43.                 'django.contrib.auth.context_processors.auth',
  44.                 'django.contrib.messages.context_processors.messages',
  45.             ],
  46.         },
  47.     },
  48. ]
  49. # WSGI应用
  50. WSGI_APPLICATION = 'myproject.wsgi.application'
  51. # 数据库配置
  52. DATABASES = {
  53.     'default': {
  54.         'ENGINE': 'django.db.backends.sqlite3',
  55.         'NAME': BASE_DIR / 'db.sqlite3',
  56.     }
  57. }
  58. # 密码验证
  59. AUTH_PASSWORD_VALIDATORS = [
  60.     {
  61.         'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
  62.     },
  63.     {
  64.         'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
  65.     },
  66.     {
  67.         'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
  68.     },
  69.     {
  70.         'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
  71.     },
  72. ]
  73. # 国际化
  74. LANGUAGE_CODE = 'en-us'
  75. TIME_ZONE = 'UTC'
  76. USE_I18N = True
  77. USE_L10N = True
  78. USE_TZ = True
  79. # 静态文件
  80. STATIC_URL = '/static/'
  81. STATICFILES_DIRS = [
  82.     BASE_DIR / 'static',
  83. ]
  84. # 默认主键字段类型
  85. DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
复制代码

2.4 创建应用

在Django中,应用是项目的功能模块。创建一个应用:
  1. # 创建应用
  2. python manage.py startapp blog
  3. # 目录结构
  4. blog/
  5.     __init__.py
  6.     admin.py
  7.     apps.py
  8.     migrations/
  9.     models.py
  10.     tests.py
  11.     views.py
复制代码

将新创建的应用添加到settings.py的INSTALLED_APPS中:
  1. # settings.py
  2. INSTALLED_APPS = [
  3.     # ...其他应用
  4.     'blog',  # 添加我们的应用
  5. ]
复制代码

3. 数据库设计

3.1 模型定义

模型是数据的单一、明确的信息源。在blog/models.py中定义模型:
  1. # blog/models.py
  2. from django.db import models
  3. from django.contrib.auth.models import User
  4. from django.utils import timezone
  5. class Category(models.Model):
  6.     name = models.CharField(max_length=100, unique=True)
  7.     slug = models.SlugField(max_length=100, unique=True)
  8.     description = models.TextField(blank=True)
  9.    
  10.     class Meta:
  11.         verbose_name_plural = 'Categories'
  12.    
  13.     def __str__(self):
  14.         return self.name
  15. class Tag(models.Model):
  16.     name = models.CharField(max_length=100, unique=True)
  17.     slug = models.SlugField(max_length=100, unique=True)
  18.    
  19.     def __str__(self):
  20.         return self.name
  21. class Post(models.Model):
  22.     STATUS_CHOICES = (
  23.         ('draft', 'Draft'),
  24.         ('published', 'Published'),
  25.     )
  26.    
  27.     title = models.CharField(max_length=200, unique=True)
  28.     slug = models.SlugField(max_length=200, unique=True)
  29.     author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
  30.     content = models.TextField()
  31.     featured_image = models.ImageField(upload_to='post_images/', blank=True)
  32.     created_at = models.DateTimeField(auto_now_add=True)
  33.     updated_at = models.DateTimeField(auto_now=True)
  34.     status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
  35.     categories = models.ManyToManyField(Category, related_name='posts')
  36.     tags = models.ManyToManyField(Tag, related_name='posts', blank=True)
  37.    
  38.     class Meta:
  39.         ordering = ['-created_at']
  40.    
  41.     def __str__(self):
  42.         return self.title
  43. class Comment(models.Model):
  44.     post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
  45.     name = models.CharField(max_length=100)
  46.     email = models.EmailField()
  47.     content = models.TextField()
  48.     created_at = models.DateTimeField(auto_now_add=True)
  49.     approved = models.BooleanField(default=True)
  50.    
  51.     class Meta:
  52.         ordering = ['created_at']
  53.    
  54.     def __str__(self):
  55.         return f'Comment by {self.name} on {self.post}'
复制代码

3.2 数据库迁移

创建模型后,需要生成并应用迁移文件:
  1. # 生成迁移文件
  2. python manage.py makemigrations
  3. # 应用迁移
  4. python manage.py migrate
复制代码

3.3 注册模型到管理界面

为了在Django管理界面中管理模型,在blog/admin.py中注册它们:
  1. # blog/admin.py
  2. from django.contrib import admin
  3. from .models import Category, Tag, Post, Comment
  4. @admin.register(Category)
  5. class CategoryAdmin(admin.ModelAdmin):
  6.     list_display = ['name', 'slug']
  7.     prepopulated_fields = {'slug': ('name',)}
  8. @admin.register(Tag)
  9. class TagAdmin(admin.ModelAdmin):
  10.     list_display = ['name', 'slug']
  11.     prepopulated_fields = {'slug': ('name',)}
  12. @admin.register(Post)
  13. class PostAdmin(admin.ModelAdmin):
  14.     list_display = ['title', 'author', 'status', 'created_at']
  15.     list_filter = ['status', 'created_at', 'author']
  16.     search_fields = ['title', 'content']
  17.     prepopulated_fields = {'slug': ('title',)}
  18.     raw_id_fields = ['author']
  19.     date_hierarchy = 'created_at'
  20.     ordering = ['status', '-created_at']
  21.     filter_horizontal = ['categories', 'tags']
  22. @admin.register(Comment)
  23. class CommentAdmin(admin.ModelAdmin):
  24.     list_display = ['name', 'post', 'created_at', 'approved']
  25.     list_filter = ['approved', 'created_at']
  26.     search_fields = ['name', 'email', 'content']
复制代码

3.4 创建超级用户

为了访问Django管理界面,需要创建一个超级用户:
  1. # 创建超级用户
  2. python manage.py createsuperuser
复制代码

按照提示输入用户名、邮箱和密码。

4. 模板创建

4.1 项目模板结构

在项目根目录下创建模板目录结构:
  1. myproject/
  2.     templates/
  3.         base.html
  4.         blog/
  5.             post_list.html
  6.             post_detail.html
  7.             post_form.html
  8.     static/
  9.         css/
  10.             style.css
  11.         js/
  12.             main.js
  13.         images/
复制代码

4.2 基础模板

创建templates/base.html作为基础模板:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>{% block title %}My Blog{% endblock %}</title>
  7.     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
  8.     <link rel="stylesheet" href="{% static 'css/style.css' %}">
  9.     {% block extra_css %}{% endblock %}
  10. </head>
  11. <body>
  12.     <header>
  13.         <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  14.             <div class="container">
  15.                 <a class="navbar-brand" href="{% url 'blog:post_list' %}">My Blog</a>
  16.                 <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav">
  17.                     <span class="navbar-toggler-icon"></span>
  18.                 </button>
  19.                 <div class="collapse navbar-collapse" id="navbarNav">
  20.                     <ul class="navbar-nav mr-auto">
  21.                         <li class="nav-item">
  22.                             <a class="nav-link" href="{% url 'blog:post_list' %}">Home</a>
  23.                         </li>
  24.                         <li class="nav-item">
  25.                             <a class="nav-link" href="#">About</a>
  26.                         </li>
  27.                         <li class="nav-item">
  28.                             <a class="nav-link" href="#">Contact</a>
  29.                         </li>
  30.                     </ul>
  31.                     <ul class="navbar-nav">
  32.                         {% if user.is_authenticated %}
  33.                             <li class="nav-item">
  34.                                 <a class="nav-link" href="{% url 'admin:index' %}">Admin</a>
  35.                             </li>
  36.                             <li class="nav-item">
  37.                                 <a class="nav-link" href="{% url 'logout' %}">Logout</a>
  38.                             </li>
  39.                         {% else %}
  40.                             <li class="nav-item">
  41.                                 <a class="nav-link" href="{% url 'login' %}">Login</a>
  42.                             </li>
  43.                         {% endif %}
  44.                     </ul>
  45.                 </div>
  46.             </div>
  47.         </nav>
  48.     </header>
  49.     <main class="container mt-4">
  50.         <div class="row">
  51.             <div class="col-md-8">
  52.                 {% block content %}{% endblock %}
  53.             </div>
  54.             <div class="col-md-4">
  55.                 <aside>
  56.                     <div class="card mb-4">
  57.                         <div class="card-header">
  58.                             <h5>Categories</h5>
  59.                         </div>
  60.                         <div class="card-body">
  61.                             <ul class="list-group">
  62.                                 {% for category in categories %}
  63.                                     <li class="list-group-item">
  64.                                         <a href="{% url 'blog:post_list_by_category' category.slug %}">{{ category.name }}</a>
  65.                                         <span class="badge badge-primary float-right">{{ category.posts.count }}</span>
  66.                                     </li>
  67.                                 {% endfor %}
  68.                             </ul>
  69.                         </div>
  70.                     </div>
  71.                     
  72.                     <div class="card mb-4">
  73.                         <div class="card-header">
  74.                             <h5>Tags</h5>
  75.                         </div>
  76.                         <div class="card-body">
  77.                             {% for tag in tags %}
  78.                                 <a href="{% url 'blog:post_list_by_tag' tag.slug %}" class="badge badge-info mr-1">{{ tag.name }}</a>
  79.                             {% endfor %}
  80.                         </div>
  81.                     </div>
  82.                 </aside>
  83.             </div>
  84.         </div>
  85.     </main>
  86.     <footer class="bg-dark text-white py-4 mt-5">
  87.         <div class="container">
  88.             <div class="row">
  89.                 <div class="col-md-6">
  90.                     <h5>My Blog</h5>
  91.                     <p>A simple blog built with Django</p>
  92.                 </div>
  93.                 <div class="col-md-6 text-md-right">
  94.                     <p>&copy; {% now "Y" %} My Blog. All rights reserved.</p>
  95.                 </div>
  96.             </div>
  97.         </div>
  98.     </footer>
  99.     <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
  100.     <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
  101.     <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
  102.     <script src="{% static 'js/main.js' %}"></script>
  103.     {% block extra_js %}{% endblock %}
  104. </body>
  105. </html>
复制代码

4.3 文章列表模板

创建templates/blog/post_list.html:
  1. {% extends 'base.html' %}
  2. {% load static %}
  3. {% block title %}Blog - {{ block.super }}{% endblock %}
  4. {% block content %}
  5.     <h1>Blog Posts</h1>
  6.    
  7.     {% for post in posts %}
  8.         <article class="card mb-4">
  9.             {% if post.featured_image %}
  10.                 <img src="{{ post.featured_image.url }}" class="card-img-top" alt="{{ post.title }}">
  11.             {% endif %}
  12.             <div class="card-body">
  13.                 <h2 class="card-title">
  14.                     <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
  15.                 </h2>
  16.                 <p class="text-muted">
  17.                     By {{ post.author }} on {{ post.created_at|date:"F d, Y" }}
  18.                 </p>
  19.                 <p class="card-text">{{ post.content|truncatewords:30 }}</p>
  20.                 <a href="{{ post.get_absolute_url }}" class="btn btn-primary">Read More &rarr;</a>
  21.             </div>
  22.             <div class="card-footer text-muted">
  23.                 <div class="d-flex justify-content-between">
  24.                     <div>
  25.                         Categories:
  26.                         {% for category in post.categories.all %}
  27.                             <a href="{% url 'blog:post_list_by_category' category.slug %}">{{ category.name }}</a>{% if not forloop.last %}, {% endif %}
  28.                         {% endfor %}
  29.                     </div>
  30.                     <div>
  31.                         Tags:
  32.                         {% for tag in post.tags.all %}
  33.                             <a href="{% url 'blog:post_list_by_tag' tag.slug %}">{{ tag.name }}</a>{% if not forloop.last %}, {% endif %}
  34.                         {% endfor %}
  35.                     </div>
  36.                 </div>
  37.             </div>
  38.         </article>
  39.     {% empty %}
  40.         <p>No posts found.</p>
  41.     {% endfor %}
  42.    
  43.     <!-- Pagination -->
  44.     {% if is_paginated %}
  45.         <nav aria-label="Page navigation">
  46.             <ul class="pagination justify-content-center">
  47.                 {% if page_obj.has_previous %}
  48.                     <li class="page-item">
  49.                         <a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">
  50.                             <span aria-hidden="true">&laquo;</span>
  51.                         </a>
  52.                     </li>
  53.                 {% endif %}
  54.                
  55.                 {% for num in page_obj.paginator.page_range %}
  56.                     {% if page_obj.number == num %}
  57.                         <li class="page-item active">
  58.                             <span class="page-link">{{ num }}</span>
  59.                         </li>
  60.                     {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
  61.                         <li class="page-item">
  62.                             <a class="page-link" href="?page={{ num }}">{{ num }}</a>
  63.                         </li>
  64.                     {% endif %}
  65.                 {% endfor %}
  66.                
  67.                 {% if page_obj.has_next %}
  68.                     <li class="page-item">
  69.                         <a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
  70.                             <span aria-hidden="true">&raquo;</span>
  71.                         </a>
  72.                     </li>
  73.                 {% endif %}
  74.             </ul>
  75.         </nav>
  76.     {% endif %}
  77. {% endblock %}
复制代码

4.4 文章详情模板

创建templates/blog/post_detail.html:
  1. {% extends 'base.html' %}
  2. {% load static %}
  3. {% block title %}{{ post.title }} - {{ block.super }}{% endblock %}
  4. {% block content %}
  5.     <article class="card mb-4">
  6.         {% if post.featured_image %}
  7.             <img src="{{ post.featured_image.url }}" class="card-img-top" alt="{{ post.title }}">
  8.         {% endif %}
  9.         <div class="card-body">
  10.             <h1 class="card-title">{{ post.title }}</h1>
  11.             <p class="text-muted">
  12.                 By {{ post.author }} on {{ post.created_at|date:"F d, Y" }}
  13.             </p>
  14.             <div class="card-text">
  15.                 {{ post.content|linebreaks }}
  16.             </div>
  17.             
  18.             <div class="mt-4">
  19.                 <div class="mb-2">
  20.                     <strong>Categories:</strong>
  21.                     {% for category in post.categories.all %}
  22.                         <a href="{% url 'blog:post_list_by_category' category.slug %}" class="badge badge-info">{{ category.name }}</a>
  23.                     {% endfor %}
  24.                 </div>
  25.                 <div>
  26.                     <strong>Tags:</strong>
  27.                     {% for tag in post.tags.all %}
  28.                         <a href="{% url 'blog:post_list_by_tag' tag.slug %}" class="badge badge-secondary">{{ tag.name }}</a>
  29.                     {% endfor %}
  30.                 </div>
  31.             </div>
  32.         </div>
  33.     </article>
  34.    
  35.     <!-- Comments Section -->
  36.     <div class="card mb-4">
  37.         <div class="card-header">
  38.             <h4>Comments ({{ post.comments.count }})</h4>
  39.         </div>
  40.         <div class="card-body">
  41.             {% for comment in post.comments.all %}
  42.                 {% if comment.approved %}
  43.                     <div class="media mb-4">
  44.                         <div class="media-body">
  45.                             <h5 class="mt-0">{{ comment.name }}</h5>
  46.                             <p class="text-muted">{{ comment.created_at|date:"F d, Y, P" }}</p>
  47.                             <p>{{ comment.content|linebreaks }}</p>
  48.                         </div>
  49.                     </div>
  50.                 {% endif %}
  51.             {% empty %}
  52.                 <p>No comments yet.</p>
  53.             {% endfor %}
  54.             
  55.             <!-- Comment Form -->
  56.             <hr>
  57.             <h5>Leave a Comment</h5>
  58.             <form method="post" action="{% url 'blog:add_comment' post.id %}">
  59.                 {% csrf_token %}
  60.                 <div class="form-group">
  61.                     <label for="id_name">Name</label>
  62.                     <input type="text" class="form-control" name="name" id="id_name" required>
  63.                 </div>
  64.                 <div class="form-group">
  65.                     <label for="id_email">Email</label>
  66.                     <input type="email" class="form-control" name="email" id="id_email" required>
  67.                 </div>
  68.                 <div class="form-group">
  69.                     <label for="id_content">Comment</label>
  70.                     <textarea class="form-control" name="content" id="id_content" rows="3" required></textarea>
  71.                 </div>
  72.                 <button type="submit" class="btn btn-primary">Submit</button>
  73.             </form>
  74.         </div>
  75.     </div>
  76. {% endblock %}
复制代码

4.5 文章表单模板

创建templates/blog/post_form.html:
  1. {% extends 'base.html' %}
  2. {% load static %}
  3. {% block title %}{% if form.instance.pk %}Edit Post{% else %}New Post{% endif %} - {{ block.super }}{% endblock %}
  4. {% block content %}
  5.     <h1>{% if form.instance.pk %}Edit Post{% else %}New Post{% endif %}</h1>
  6.    
  7.     <form method="post" enctype="multipart/form-data">
  8.         {% csrf_token %}
  9.         <div class="form-group">
  10.             <label for="{{ form.title.id_for_label }}">Title</label>
  11.             {{ form.title.errors }}
  12.             <input type="text" class="form-control" name="{{ form.title.name }}" id="{{ form.title.id_for_label }}" value="{{ form.title.value|default:'' }}" required>
  13.         </div>
  14.         
  15.         <div class="form-group">
  16.             <label for="{{ form.slug.id_for_label }}">Slug</label>
  17.             {{ form.slug.errors }}
  18.             <input type="text" class="form-control" name="{{ form.slug.name }}" id="{{ form.slug.id_for_label }}" value="{{ form.slug.value|default:'' }}" required>
  19.         </div>
  20.         
  21.         <div class="form-group">
  22.             <label for="{{ form.content.id_for_label }}">Content</label>
  23.             {{ form.content.errors }}
  24.             <textarea class="form-control" name="{{ form.content.name }}" id="{{ form.content.id_for_label }}" rows="10" required>{{ form.content.value|default:'' }}</textarea>
  25.         </div>
  26.         
  27.         <div class="form-group">
  28.             <label for="{{ form.featured_image.id_for_label }}">Featured Image</label>
  29.             {{ form.featured_image.errors }}
  30.             <input type="file" class="form-control-file" name="{{ form.featured_image.name }}" id="{{ form.featured_image.id_for_label }}">
  31.             {% if form.instance.featured_image %}
  32.                 <img src="{{ form.instance.featured_image.url }}" class="img-thumbnail mt-2" alt="{{ form.instance.title }}">
  33.             {% endif %}
  34.         </div>
  35.         
  36.         <div class="form-group">
  37.             <label for="{{ form.status.id_for_label }}">Status</label>
  38.             {{ form.status.errors }}
  39.             <select class="form-control" name="{{ form.status.name }}" id="{{ form.status.id_for_label }}">
  40.                 {% for value, label in form.status.field.choices %}
  41.                     <option value="{{ value }}" {% if form.status.value == value %}selected{% endif %}>{{ label }}</option>
  42.                 {% endfor %}
  43.             </select>
  44.         </div>
  45.         
  46.         <div class="form-group">
  47.             <label for="{{ form.categories.id_for_label }}">Categories</label>
  48.             {{ form.categories.errors }}
  49.             <select multiple class="form-control" name="{{ form.categories.name }}" id="{{ form.categories.id_for_label }}" size="5">
  50.                 {% for category in form.categories.field.queryset %}
  51.                     <option value="{{ category.id }}" {% if category.id in form.categories.value %}selected{% endif %}>{{ category.name }}</option>
  52.                 {% endfor %}
  53.             </select>
  54.             <small class="form-text text-muted">Hold Ctrl (or Cmd on Mac) to select multiple categories.</small>
  55.         </div>
  56.         
  57.         <div class="form-group">
  58.             <label for="{{ form.tags.id_for_label }}">Tags</label>
  59.             {{ form.tags.errors }}
  60.             <select multiple class="form-control" name="{{ form.tags.name }}" id="{{ form.tags.id_for_label }}" size="5">
  61.                 {% for tag in form.tags.field.queryset %}
  62.                     <option value="{{ tag.id }}" {% if tag.id in form.tags.value %}selected{% endif %}>{{ tag.name }}</option>
  63.                 {% endfor %}
  64.             </select>
  65.             <small class="form-text text-muted">Hold Ctrl (or Cmd on Mac) to select multiple tags.</small>
  66.         </div>
  67.         
  68.         <button type="submit" class="btn btn-primary">Save</button>
  69.         <a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
  70.     </form>
  71. {% endblock %}
复制代码

5. 视图函数

5.1 基本视图

在blog/views.py中创建视图函数:
  1. # blog/views.py
  2. from django.shortcuts import render, get_object_or_404, redirect
  3. from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
  4. from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
  5. from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
  6. from django.urls import reverse_lazy
  7. from django.db.models import Count
  8. from .models import Post, Category, Tag, Comment
  9. from .forms import PostForm, CommentForm
  10. class PostListView(ListView):
  11.     model = Post
  12.     template_name = 'blog/post_list.html'
  13.     context_object_name = 'posts'
  14.     paginate_by = 5
  15.    
  16.     def get_queryset(self):
  17.         return Post.objects.filter(status='published')
  18.    
  19.     def get_context_data(self, **kwargs):
  20.         context = super().get_context_data(**kwargs)
  21.         context['categories'] = Category.objects.all()
  22.         context['tags'] = Tag.objects.all()
  23.         return context
  24. class PostDetailView(DetailView):
  25.     model = Post
  26.     template_name = 'blog/post_detail.html'
  27.     context_object_name = 'post'
  28.    
  29.     def get_context_data(self, **kwargs):
  30.         context = super().get_context_data(**kwargs)
  31.         context['categories'] = Category.objects.all()
  32.         context['tags'] = Tag.objects.all()
  33.         return context
  34. class PostCreateView(LoginRequiredMixin, CreateView):
  35.     model = Post
  36.     form_class = PostForm
  37.     template_name = 'blog/post_form.html'
  38.    
  39.     def form_valid(self, form):
  40.         form.instance.author = self.request.user
  41.         return super().form_valid(form)
  42. class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
  43.     model = Post
  44.     form_class = PostForm
  45.     template_name = 'blog/post_form.html'
  46.    
  47.     def test_func(self):
  48.         post = self.get_object()
  49.         return self.request.user == post.author
  50. class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
  51.     model = Post
  52.     template_name = 'blog/post_confirm_delete.html'
  53.     success_url = reverse_lazy('blog:post_list')
  54.    
  55.     def test_func(self):
  56.         post = self.get_object()
  57.         return self.request.user == post.author
  58. def post_list_by_category(request, category_slug):
  59.     category = get_object_or_404(Category, slug=category_slug)
  60.     posts = Post.objects.filter(status='published', categories=category)
  61.    
  62.     paginator = Paginator(posts, 5)
  63.     page = request.GET.get('page')
  64.    
  65.     try:
  66.         posts = paginator.page(page)
  67.     except PageNotAnInteger:
  68.         posts = paginator.page(1)
  69.     except EmptyPage:
  70.         posts = paginator.page(paginator.num_pages)
  71.    
  72.     categories = Category.objects.all()
  73.     tags = Tag.objects.all()
  74.    
  75.     return render(request, 'blog/post_list.html', {
  76.         'posts': posts,
  77.         'category': category,
  78.         'categories': categories,
  79.         'tags': tags,
  80.         'is_paginated': True,
  81.         'page_obj': posts,
  82.     })
  83. def post_list_by_tag(request, tag_slug):
  84.     tag = get_object_or_404(Tag, slug=tag_slug)
  85.     posts = Post.objects.filter(status='published', tags=tag)
  86.    
  87.     paginator = Paginator(posts, 5)
  88.     page = request.GET.get('page')
  89.    
  90.     try:
  91.         posts = paginator.page(page)
  92.     except PageNotAnInteger:
  93.         posts = paginator.page(1)
  94.     except EmptyPage:
  95.         posts = paginator.page(paginator.num_pages)
  96.    
  97.     categories = Category.objects.all()
  98.     tags = Tag.objects.all()
  99.    
  100.     return render(request, 'blog/post_list.html', {
  101.         'posts': posts,
  102.         'tag': tag,
  103.         'categories': categories,
  104.         'tags': tags,
  105.         'is_paginated': True,
  106.         'page_obj': posts,
  107.     })
  108. def add_comment(request, post_id):
  109.     post = get_object_or_404(Post, id=post_id)
  110.    
  111.     if request.method == 'POST':
  112.         name = request.POST.get('name')
  113.         email = request.POST.get('email')
  114.         content = request.POST.get('content')
  115.         
  116.         if name and email and content:
  117.             comment = Comment(
  118.                 post=post,
  119.                 name=name,
  120.                 email=email,
  121.                 content=content
  122.             )
  123.             comment.save()
  124.    
  125.     return redirect('blog:post_detail', pk=post.id)
复制代码

5.2 表单类

在blog目录下创建forms.py文件:
  1. # blog/forms.py
  2. from django import forms
  3. from .models import Post, Comment
  4. class PostForm(forms.ModelForm):
  5.     class Meta:
  6.         model = Post
  7.         fields = ['title', 'slug', 'content', 'featured_image', 'status', 'categories', 'tags']
  8.         
  9.     def __init__(self, *args, **kwargs):
  10.         super().__init__(*args, **kwargs)
  11.         self.fields['categories'].widget.attrs.update({'size': '5'})
  12.         self.fields['tags'].widget.attrs.update({'size': '5'})
  13. class CommentForm(forms.ModelForm):
  14.     class Meta:
  15.         model = Comment
  16.         fields = ['name', 'email', 'content']
复制代码

5.3 URL配置

在blog目录下创建urls.py文件:
  1. # blog/urls.py
  2. from django.urls import path
  3. from . import views
  4. app_name = 'blog'
  5. urlpatterns = [
  6.     path('', views.PostListView.as_view(), name='post_list'),
  7.     path('post/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),
  8.     path('post/new/', views.PostCreateView.as_view(), name='post_create'),
  9.     path('post/<int:pk>/edit/', views.PostUpdateView.as_view(), name='post_update'),
  10.     path('post/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
  11.     path('category/<slug:category_slug>/', views.post_list_by_category, name='post_list_by_category'),
  12.     path('tag/<slug:tag_slug>/', views.post_list_by_tag, name='post_list_by_tag'),
  13.     path('post/<int:post_id>/comment/', views.add_comment, name='add_comment'),
  14. ]
复制代码

5.4 项目URL配置

更新项目的urls.py文件:
  1. # myproject/urls.py
  2. from django.contrib import admin
  3. from django.urls import path, include
  4. from django.conf import settings
  5. from django.conf.urls.static import static
  6. urlpatterns = [
  7.     path('admin/', admin.site.urls),
  8.     path('', include('blog.urls')),
  9. ]
  10. if settings.DEBUG:
  11.     urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
复制代码

6. 高级功能

6.1 搜索功能

在blog/views.py中添加搜索视图:
  1. # blog/views.py
  2. from django.db.models import Q
  3. def search(request):
  4.     query = request.GET.get('q')
  5.     posts = Post.objects.filter(status='published')
  6.    
  7.     if query:
  8.         posts = posts.filter(
  9.             Q(title__icontains=query) |
  10.             Q(content__icontains=query) |
  11.             Q(categories__name__icontains=query) |
  12.             Q(tags__name__icontains=query)
  13.         ).distinct()
  14.    
  15.     paginator = Paginator(posts, 5)
  16.     page = request.GET.get('page')
  17.    
  18.     try:
  19.         posts = paginator.page(page)
  20.     except PageNotAnInteger:
  21.         posts = paginator.page(1)
  22.     except EmptyPage:
  23.         posts = paginator.page(paginator.num_pages)
  24.    
  25.     categories = Category.objects.all()
  26.     tags = Tag.objects.all()
  27.    
  28.     return render(request, 'blog/post_list.html', {
  29.         'posts': posts,
  30.         'query': query,
  31.         'categories': categories,
  32.         'tags': tags,
  33.         'is_paginated': True,
  34.         'page_obj': posts,
  35.     })
复制代码

在blog/urls.py中添加搜索URL:
  1. # blog/urls.py
  2. urlpatterns = [
  3.     # ... 其他URL
  4.     path('search/', views.search, name='search'),
  5. ]
复制代码

在基础模板中添加搜索表单:
  1. <!-- 在base.html的navbar中添加 -->
  2. <form class="form-inline my-2 my-lg-0" action="{% url 'blog:search' %}" method="get">
  3.     <input class="form-control mr-sm-2" type="search" name="q" placeholder="Search" aria-label="Search">
  4.     <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
  5. </form>
复制代码

6.2 RSS订阅

在blog目录下创建feeds.py文件:
  1. # blog/feeds.py
  2. from django.contrib.syndication.views import Feed
  3. from django.urls import reverse
  4. from django.utils.feedgenerator import Rss201rev2Feed
  5. from .models import Post
  6. class LatestPostsFeed(Feed):
  7.     feed_type = Rss201rev2Feed
  8.     title = "My Blog"
  9.     link = "/blog/"
  10.     description = "Latest posts from My Blog"
  11.    
  12.     def items(self):
  13.         return Post.objects.filter(status='published')[:10]
  14.    
  15.     def item_title(self, item):
  16.         return item.title
  17.    
  18.     def item_description(self, item):
  19.         return item.content[:200] + "..."
  20.    
  21.     def item_link(self, item):
  22.         return reverse('blog:post_detail', args=[item.pk])
  23.    
  24.     def item_pubdate(self, item):
  25.         return item.created_at
复制代码

在blog/urls.py中添加RSS订阅URL:
  1. # blog/urls.py
  2. from django.urls import path
  3. from . import views
  4. from .feeds import LatestPostsFeed
  5. app_name = 'blog'
  6. urlpatterns = [
  7.     # ... 其他URL
  8.     path('feed/', LatestPostsFeed(), name='post_feed'),
  9. ]
复制代码

在基础模板中添加RSS链接:
  1. <!-- 在base.html的head部分添加 -->
  2. <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="{% url 'blog:post_feed' %}">
复制代码

6.3 站点地图

在blog目录下创建sitemaps.py文件:
  1. # blog/sitemaps.py
  2. from django.contrib.sitemaps import Sitemap
  3. from django.urls import reverse
  4. from .models import Post, Category, Tag
  5. class PostSitemap(Sitemap):
  6.     changefreq = "weekly"
  7.     priority = 0.9
  8.    
  9.     def items(self):
  10.         return Post.objects.filter(status='published')
  11.    
  12.     def lastmod(self, obj):
  13.         return obj.updated_at
  14.    
  15.     def location(self, obj):
  16.         return reverse('blog:post_detail', args=[obj.pk])
  17. class CategorySitemap(Sitemap):
  18.     changefreq = "monthly"
  19.     priority = 0.7
  20.    
  21.     def items(self):
  22.         return Category.objects.all()
  23.    
  24.     def location(self, obj):
  25.         return reverse('blog:post_list_by_category', args=[obj.slug])
  26. class TagSitemap(Sitemap):
  27.     changefreq = "monthly"
  28.     priority = 0.6
  29.    
  30.     def items(self):
  31.         return Tag.objects.all()
  32.    
  33.     def location(self, obj):
  34.         return reverse('blog:post_list_by_tag', args=[obj.slug])
  35. class StaticViewSitemap(Sitemap):
  36.     priority = 0.5
  37.     changefreq = 'monthly'
  38.    
  39.     def items(self):
  40.         return ['blog:post_list']
  41.    
  42.     def location(self, item):
  43.         return reverse(item)
复制代码

在项目的urls.py中添加站点地图URL:
  1. # myproject/urls.py
  2. from django.contrib import admin
  3. from django.urls import path, include
  4. from django.contrib.sitemaps.views import sitemap
  5. from blog.sitemaps import PostSitemap, CategorySitemap, TagSitemap, StaticViewSitemap
  6. sitemaps = {
  7.     'posts': PostSitemap,
  8.     'categories': CategorySitemap,
  9.     'tags': TagSitemap,
  10.     'static': StaticViewSitemap,
  11. }
  12. urlpatterns = [
  13.     path('admin/', admin.site.urls),
  14.     path('', include('blog.urls')),
  15.     path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
  16. ]
复制代码

7. 测试

7.1 模型测试

在blog/tests.py中编写模型测试:
  1. # blog/tests.py
  2. from django.test import TestCase
  3. from django.contrib.auth.models import User
  4. from .models import Post, Category, Tag, Comment
  5. class CategoryModelTest(TestCase):
  6.     def setUp(self):
  7.         self.category = Category.objects.create(
  8.             name="Test Category",
  9.             slug="test-category",
  10.             description="Test category description"
  11.         )
  12.    
  13.     def test_category_creation(self):
  14.         self.assertEqual(self.category.name, "Test Category")
  15.         self.assertEqual(self.category.slug, "test-category")
  16.         self.assertEqual(self.category.description, "Test category description")
  17.         self.assertEqual(str(self.category), "Test Category")
  18. class TagModelTest(TestCase):
  19.     def setUp(self):
  20.         self.tag = Tag.objects.create(
  21.             name="Test Tag",
  22.             slug="test-tag"
  23.         )
  24.    
  25.     def test_tag_creation(self):
  26.         self.assertEqual(self.tag.name, "Test Tag")
  27.         self.assertEqual(self.tag.slug, "test-tag")
  28.         self.assertEqual(str(self.tag), "Test Tag")
  29. class PostModelTest(TestCase):
  30.     def setUp(self):
  31.         self.user = User.objects.create_user(
  32.             username='testuser',
  33.             email='test@example.com',
  34.             password='testpass123'
  35.         )
  36.         self.category = Category.objects.create(
  37.             name="Test Category",
  38.             slug="test-category"
  39.         )
  40.         self.tag = Tag.objects.create(
  41.             name="Test Tag",
  42.             slug="test-tag"
  43.         )
  44.         self.post = Post.objects.create(
  45.             title="Test Post",
  46.             slug="test-post",
  47.             author=self.user,
  48.             content="This is a test post content.",
  49.             status="published"
  50.         )
  51.         self.post.categories.add(self.category)
  52.         self.post.tags.add(self.tag)
  53.    
  54.     def test_post_creation(self):
  55.         self.assertEqual(self.post.title, "Test Post")
  56.         self.assertEqual(self.post.slug, "test-post")
  57.         self.assertEqual(self.post.author, self.user)
  58.         self.assertEqual(self.post.content, "This is a test post content.")
  59.         self.assertEqual(self.post.status, "published")
  60.         self.assertEqual(str(self.post), "Test Post")
  61.         self.assertIn(self.category, self.post.categories.all())
  62.         self.assertIn(self.tag, self.post.tags.all())
  63. class CommentModelTest(TestCase):
  64.     def setUp(self):
  65.         self.user = User.objects.create_user(
  66.             username='testuser',
  67.             email='test@example.com',
  68.             password='testpass123'
  69.         )
  70.         self.post = Post.objects.create(
  71.             title="Test Post",
  72.             slug="test-post",
  73.             author=self.user,
  74.             content="This is a test post content.",
  75.             status="published"
  76.         )
  77.         self.comment = Comment.objects.create(
  78.             post=self.post,
  79.             name="Test Commenter",
  80.             email="commenter@example.com",
  81.             content="This is a test comment."
  82.         )
  83.    
  84.     def test_comment_creation(self):
  85.         self.assertEqual(self.comment.post, self.post)
  86.         self.assertEqual(self.comment.name, "Test Commenter")
  87.         self.assertEqual(self.comment.email, "commenter@example.com")
  88.         self.assertEqual(self.comment.content, "This is a test comment.")
  89.         self.assertTrue(self.comment.approved)
  90.         self.assertEqual(str(self.comment), 'Comment by Test Commenter on Test Post')
复制代码

7.2 视图测试

继续在blog/tests.py中添加视图测试:
  1. # blog/tests.py (继续)
  2. from django.test import Client
  3. from django.urls import reverse
  4. class PostListViewTest(TestCase):
  5.     def setUp(self):
  6.         self.client = Client()
  7.         self.user = User.objects.create_user(
  8.             username='testuser',
  9.             email='test@example.com',
  10.             password='testpass123'
  11.         )
  12.         self.category = Category.objects.create(
  13.             name="Test Category",
  14.             slug="test-category"
  15.         )
  16.         self.tag = Tag.objects.create(
  17.             name="Test Tag",
  18.             slug="test-tag"
  19.         )
  20.         # Create 10 published posts
  21.         for i in range(10):
  22.             Post.objects.create(
  23.                 title=f"Test Post {i}",
  24.                 slug=f"test-post-{i}",
  25.                 author=self.user,
  26.                 content=f"This is test post {i} content.",
  27.                 status="published"
  28.             )
  29.         # Create 2 draft posts
  30.         for i in range(2):
  31.             Post.objects.create(
  32.                 title=f"Draft Post {i}",
  33.                 slug=f"draft-post-{i}",
  34.                 author=self.user,
  35.                 content=f"This is draft post {i} content.",
  36.                 status="draft"
  37.             )
  38.    
  39.     def test_view_url_exists_at_desired_location(self):
  40.         response = self.client.get('')
  41.         self.assertEqual(response.status_code, 200)
  42.    
  43.     def test_view_url_accessible_by_name(self):
  44.         response = self.client.get(reverse('blog:post_list'))
  45.         self.assertEqual(response.status_code, 200)
  46.    
  47.     def test_view_uses_correct_template(self):
  48.         response = self.client.get(reverse('blog:post_list'))
  49.         self.assertEqual(response.status_code, 200)
  50.         self.assertTemplateUsed(response, 'blog/post_list.html')
  51.    
  52.     def test_pagination_is_five(self):
  53.         response = self.client.get(reverse('blog:post_list'))
  54.         self.assertEqual(response.status_code, 200)
  55.         self.assertTrue('is_paginated' in response.context)
  56.         self.assertTrue(response.context['is_paginated'] == True)
  57.         self.assertEqual(len(response.context['posts']), 5)
  58.    
  59.     def test_lists_all_posts(self):
  60.         # Get second page and confirm it has (exactly) remaining 5 items
  61.         response = self.client.get(reverse('blog:post_list') + '?page=2')
  62.         self.assertEqual(response.status_code, 200)
  63.         self.assertTrue('is_paginated' in response.context)
  64.         self.assertTrue(response.context['is_paginated'] == True)
  65.         self.assertEqual(len(response.context['posts']), 5)
  66.    
  67.     def test_only_published_posts(self):
  68.         response = self.client.get(reverse('blog:post_list'))
  69.         self.assertEqual(response.status_code, 200)
  70.         self.assertEqual(len(response.context['posts']), 5)
  71.         for post in response.context['posts']:
  72.             self.assertEqual(post.status, 'published')
  73. class PostDetailViewTest(TestCase):
  74.     def setUp(self):
  75.         self.client = Client()
  76.         self.user = User.objects.create_user(
  77.             username='testuser',
  78.             email='test@example.com',
  79.             password='testpass123'
  80.         )
  81.         self.post = Post.objects.create(
  82.             title="Test Post",
  83.             slug="test-post",
  84.             author=self.user,
  85.             content="This is a test post content.",
  86.             status="published"
  87.         )
  88.    
  89.     def test_view_url_exists_at_desired_location(self):
  90.         response = self.client.get(f'/post/{self.post.pk}/')
  91.         self.assertEqual(response.status_code, 200)
  92.    
  93.     def test_view_url_accessible_by_name(self):
  94.         response = self.client.get(reverse('blog:post_detail', kwargs={'pk': self.post.pk}))
  95.         self.assertEqual(response.status_code, 200)
  96.    
  97.     def test_view_uses_correct_template(self):
  98.         response = self.client.get(reverse('blog:post_detail', kwargs={'pk': self.post.pk}))
  99.         self.assertEqual(response.status_code, 200)
  100.         self.assertTemplateUsed(response, 'blog/post_detail.html')
  101.    
  102.     def test_nonexistent_post(self):
  103.         response = self.client.get(reverse('blog:post_detail', kwargs={'pk': 999}))
  104.         self.assertEqual(response.status_code, 404)
  105. class PostCreateViewTest(TestCase):
  106.     def setUp(self):
  107.         self.client = Client()
  108.         self.user = User.objects.create_user(
  109.             username='testuser',
  110.             email='test@example.com',
  111.             password='testpass123'
  112.         )
  113.         self.category = Category.objects.create(
  114.             name="Test Category",
  115.             slug="test-category"
  116.         )
  117.         self.tag = Tag.objects.create(
  118.             name="Test Tag",
  119.             slug="test-tag"
  120.         )
  121.    
  122.     def test_redirect_if_not_logged_in(self):
  123.         response = self.client.get(reverse('blog:post_create'))
  124.         self.assertRedirects(response, '/accounts/login/?next=/post/new/')
  125.    
  126.     def test_logged_in_uses_correct_template(self):
  127.         self.client.login(username='testuser', password='testpass123')
  128.         response = self.client.get(reverse('blog:post_create'))
  129.         self.assertEqual(response.status_code, 200)
  130.         self.assertTemplateUsed(response, 'blog/post_form.html')
  131.    
  132.     def test_create_post(self):
  133.         self.client.login(username='testuser', password='testpass123')
  134.         response = self.client.post(reverse('blog:post_create'), {
  135.             'title': 'New Test Post',
  136.             'slug': 'new-test-post',
  137.             'content': 'This is a new test post content.',
  138.             'status': 'published',
  139.             'categories': [self.category.id],
  140.             'tags': [self.tag.id]
  141.         })
  142.         self.assertEqual(response.status_code, 302)
  143.         self.assertTrue(Post.objects.filter(title='New Test Post').exists())
  144.         new_post = Post.objects.get(title='New Test Post')
  145.         self.assertEqual(new_post.author, self.user)
  146.         self.assertIn(self.category, new_post.categories.all())
  147.         self.assertIn(self.tag, new_post.tags.all())
复制代码

7.3 运行测试

运行测试以验证代码的正确性:
  1. # 运行所有测试
  2. python manage.py test
  3. # 运行特定应用的测试
  4. python manage.py test blog
  5. # 运行特定测试类
  6. python manage.py test blog.tests.PostModelTest
  7. # 运行特定测试方法
  8. python manage.py test blog.tests.PostModelTest.test_post_creation
复制代码

8. 部署上线

8.1 生产环境配置

创建生产环境设置文件myproject/settings_production.py:
  1. # myproject/settings_production.py
  2. from .settings import *
  3. # Security settings
  4. DEBUG = False
  5. ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
  6. # Database settings (using PostgreSQL as an example)
  7. DATABASES = {
  8.     'default': {
  9.         'ENGINE': 'django.db.backends.postgresql',
  10.         'NAME': 'mydatabase',
  11.         'USER': 'mydatabaseuser',
  12.         'PASSWORD': 'mypassword',
  13.         'HOST': 'localhost',
  14.         'PORT': '5432',
  15.     }
  16. }
  17. # Static files settings
  18. STATIC_ROOT = '/var/www/myproject/static/'
  19. MEDIA_ROOT = '/var/www/myproject/media/'
  20. # Security settings
  21. SECURE_SSL_REDIRECT = True
  22. SESSION_COOKIE_SECURE = True
  23. CSRF_COOKIE_SECURE = True
  24. SECURE_BROWSER_XSS_FILTER = True
  25. SECURE_CONTENT_TYPE_NOSNIFF = True
  26. X_FRAME_OPTIONS = 'DENY'
  27. # Email settings
  28. EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
  29. EMAIL_HOST = 'smtp.yourdomain.com'
  30. EMAIL_PORT = 587
  31. EMAIL_USE_TLS = True
  32. EMAIL_HOST_USER = 'your_email@yourdomain.com'
  33. EMAIL_HOST_PASSWORD = 'your_email_password'
  34. DEFAULT_FROM_EMAIL = 'your_email@yourdomain.com'
  35. # Logging settings
  36. LOGGING = {
  37.     'version': 1,
  38.     'disable_existing_loggers': False,
  39.     'handlers': {
  40.         'file': {
  41.             'level': 'ERROR',
  42.             'class': 'logging.FileHandler',
  43.             'filename': '/var/log/django/error.log',
  44.         },
  45.     },
  46.     'loggers': {
  47.         'django': {
  48.             'handlers': ['file'],
  49.             'level': 'ERROR',
  50.             'propagate': True,
  51.         },
  52.     },
  53. }
复制代码

8.2 收集静态文件

在部署前,收集所有静态文件到一个目录:
  1. # 设置环境变量
  2. export DJANGO_SETTINGS_MODULE=myproject.settings_production
  3. # 收集静态文件
  4. python manage.py collectstatic
复制代码

8.3 服务器配置
  1. # 更新系统
  2. sudo apt update
  3. sudo apt upgrade
  4. # 安装Python和pip
  5. sudo apt install python3 python3-pip python3-venv
  6. # 安装PostgreSQL
  7. sudo apt install postgresql postgresql-contrib
  8. # 安装Nginx
  9. sudo apt install nginx
  10. # 安装Gunicorn
  11. pip install gunicorn
复制代码
  1. # 切换到PostgreSQL用户
  2. sudo -u postgres psql
  3. # 创建数据库和用户
  4. CREATE DATABASE mydatabase;
  5. CREATE USER mydatabaseuser WITH PASSWORD 'mypassword';
  6. ALTER ROLE mydatabaseuser SET client_encoding TO 'utf8';
  7. ALTER ROLE mydatabaseuser SET default_transaction_isolation TO 'read committed';
  8. ALTER ROLE mydatabaseuser SET timezone TO 'UTC';
  9. GRANT ALL PRIVILEGES ON DATABASE mydatabase TO mydatabaseuser;
  10. \q
复制代码

创建Gunicorn服务文件/etc/systemd/system/gunicorn.service:
  1. [Unit]
  2. Description=gunicorn daemon
  3. After=network.target
  4. [Service]
  5. User=www-data
  6. Group=www-data
  7. WorkingDirectory=/var/www/myproject
  8. ExecStart=/var/www/myproject/venv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock myproject.wsgi:application
  9. [Install]
  10. WantedBy=multi-user.target
复制代码

启动Gunicorn服务:
  1. # 启动Gunicorn服务
  2. sudo systemctl start gunicorn
  3. sudo systemctl enable gunicorn
  4. # 检查服务状态
  5. sudo systemctl status gunicorn
复制代码

创建Nginx配置文件/etc/nginx/sites-available/myproject:
  1. server {
  2.     listen 80;
  3.     server_name yourdomain.com www.yourdomain.com;
  4.     location = /favicon.ico { access_log off; log_not_found off; }
  5.     location /static/ {
  6.         root /var/www/myproject;
  7.     }
  8.     location /media/ {
  9.         root /var/www/myproject;
  10.     }
  11.     location / {
  12.         include proxy_params;
  13.         proxy_pass http://unix:/run/gunicorn.sock;
  14.     }
  15. }
复制代码

启用配置并重启Nginx:
  1. # 启用站点
  2. sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/
  3. # 测试Nginx配置
  4. sudo nginx -t
  5. # 重启Nginx
  6. sudo systemctl restart nginx
复制代码

8.4 SSL证书配置

使用Let’s Encrypt获取SSL证书:
  1. # 安装Certbot
  2. sudo apt install certbot python3-certbot-nginx
  3. # 获取SSL证书
  4. sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
  5. # 测试自动续期
  6. sudo certbot renew --dry-run
复制代码

8.5 部署流程

创建部署脚本deploy.sh:
  1. #!/bin/bash
  2. # Variables
  3. PROJECT_DIR="/var/www/myproject"
  4. VENV_DIR="$PROJECT_DIR/venv"
  5. SERVICE_NAME="gunicorn"
  6. NGINX_SITES="/etc/nginx/sites-available"
  7. # Pull latest changes
  8. cd $PROJECT_DIR
  9. git pull origin main
  10. # Activate virtual environment
  11. source $VENV_DIR/bin/activate
  12. # Install dependencies
  13. pip install -r requirements.txt
  14. # Run migrations
  15. python manage.py migrate --settings=myproject.settings_production
  16. # Collect static files
  17. python manage.py collectstatic --noinput --settings=myproject.settings_production
  18. # Restart Gunicorn
  19. sudo systemctl restart $SERVICE_NAME
  20. # Restart Nginx
  21. sudo systemctl restart nginx
  22. echo "Deployment completed successfully!"
复制代码

使脚本可执行并运行:
  1. chmod +x deploy.sh
  2. ./deploy.sh
复制代码

9. 总结与展望

本教程详细介绍了从环境搭建到部署上线的完整Django项目开发流程。我们学习了如何:

1. 搭建Django开发环境
2. 创建和配置Django项目与应用
3. 设计数据库模型并进行迁移
4. 创建模板和静态文件
5. 编写视图函数和URL配置
6. 实现高级功能如搜索、RSS订阅和站点地图
7. 编写测试确保代码质量
8. 部署项目到生产环境

9.1 进一步学习的方向

如果您想继续深入学习Django,可以考虑以下方向:

1. Django REST Framework:构建RESTful API
2. Django Channels:实现WebSocket和实时功能
3. Celery:处理异步任务和定时任务
4. Docker:容器化Django应用
5. CI/CD:自动化测试和部署流程

9.2 最佳实践

在Django开发中,遵循以下最佳实践:

1. 保持代码整洁:遵循PEP 8风格指南
2. 使用版本控制:使用Git管理代码
3. 编写测试:确保代码质量和功能正确性
4. 文档化:为代码和项目编写文档
5. 安全性:遵循Django安全最佳实践
6. 性能优化:优化查询和缓存策略

9.3 结语

Django是一个功能强大且灵活的Web框架,适合构建各种规模的Web应用。通过本教程的学习,您应该已经掌握了Django开发的基本技能和流程。继续实践和探索,您将能够构建更加复杂和功能丰富的Web应用。

祝您在Django开发之旅中取得成功!
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则