|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
Django是一个基于Python的高级Web框架,它鼓励快速开发和干净、实用的设计。本教程将带您从零开始,一步步构建一个完整的Django项目,涵盖环境搭建、数据库设计、模板创建、视图函数配置以及最终部署上线的全过程。无论您是Django新手还是有一定经验的开发者,本教程都将为您提供实用的指导和最佳实践。
1. 环境搭建
1.1 Python环境安装
在开始Django开发之前,首先需要确保您的系统上安装了Python。Django 3.2+版本需要Python 3.6或更高版本。
- # 检查Python版本
- python --version
- # 或
- python3 --version
复制代码
如果尚未安装Python,请访问Python官网下载并安装适合您操作系统的版本。
1.2 虚拟环境创建
使用虚拟环境可以隔离不同项目的依赖,避免包版本冲突。推荐使用venv模块创建虚拟环境:
- # 创建虚拟环境
- python -m venv myproject_env
- # 激活虚拟环境
- # Windows
- myproject_env\Scripts\activate
- # macOS/Linux
- source myproject_env/bin/activate
复制代码
1.3 Django安装
激活虚拟环境后,使用pip安装Django:
- # 安装Django
- pip install django
- # 验证Django安装
- python -m django --version
复制代码
1.4 开发工具选择
选择一个合适的IDE或文本编辑器可以提高开发效率。推荐使用:
• PyCharm(专业版或社区版)
• Visual Studio Code(安装Python和Django插件)
• Sublime Text
2. 项目创建与配置
2.1 创建Django项目
使用Django的命令行工具创建新项目:
- # 创建项目
- django-admin startproject myproject
- # 进入项目目录
- cd myproject
复制代码
2.2 项目结构解析
创建的项目结构如下:
- myproject/
- manage.py
- myproject/
- __init__.py
- settings.py
- urls.py
- asgi.py
- wsgi.py
复制代码
• manage.py:Django的命令行工具,用于管理项目
• settings.py:项目的配置文件
• urls.py:项目的URL声明
• wsgi.py:WSGI兼容的Web服务器入口
• asgi.py:ASGI兼容的Web服务器入口
2.3 项目基本配置
编辑settings.py文件,进行基本配置:
- # settings.py
- import os
- from pathlib import Path
- # 构建路径
- BASE_DIR = Path(__file__).resolve().parent.parent
- # 安全密钥
- SECRET_KEY = 'django-insecure-your-secret-key-here'
- # 调试模式(生产环境应设为False)
- DEBUG = True
- # 允许的主机
- ALLOWED_HOSTS = []
- # 已安装的应用
- INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- ]
- # 中间件
- 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',
- ]
- # 根URL配置
- ROOT_URLCONF = 'myproject.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应用
- WSGI_APPLICATION = 'myproject.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 = 'en-us'
- TIME_ZONE = 'UTC'
- USE_I18N = True
- USE_L10N = True
- USE_TZ = True
- # 静态文件
- STATIC_URL = '/static/'
- STATICFILES_DIRS = [
- BASE_DIR / 'static',
- ]
- # 默认主键字段类型
- DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
复制代码
2.4 创建应用
在Django中,应用是项目的功能模块。创建一个应用:
- # 创建应用
- python manage.py startapp blog
- # 目录结构
- blog/
- __init__.py
- admin.py
- apps.py
- migrations/
- models.py
- tests.py
- views.py
复制代码
将新创建的应用添加到settings.py的INSTALLED_APPS中:
- # settings.py
- INSTALLED_APPS = [
- # ...其他应用
- 'blog', # 添加我们的应用
- ]
复制代码
3. 数据库设计
3.1 模型定义
模型是数据的单一、明确的信息源。在blog/models.py中定义模型:
- # 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, unique=True)
- slug = models.SlugField(max_length=100, unique=True)
- description = models.TextField(blank=True)
-
- class Meta:
- verbose_name_plural = 'Categories'
-
- def __str__(self):
- return self.name
- class Tag(models.Model):
- name = models.CharField(max_length=100, unique=True)
- slug = models.SlugField(max_length=100, unique=True)
-
- def __str__(self):
- return self.name
- class Post(models.Model):
- STATUS_CHOICES = (
- ('draft', 'Draft'),
- ('published', 'Published'),
- )
-
- title = models.CharField(max_length=200, unique=True)
- slug = models.SlugField(max_length=200, unique=True)
- author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
- content = models.TextField()
- featured_image = models.ImageField(upload_to='post_images/', blank=True)
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
- status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
- categories = models.ManyToManyField(Category, related_name='posts')
- tags = models.ManyToManyField(Tag, related_name='posts', blank=True)
-
- class Meta:
- ordering = ['-created_at']
-
- def __str__(self):
- return self.title
- class Comment(models.Model):
- post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
- name = models.CharField(max_length=100)
- email = models.EmailField()
- content = models.TextField()
- created_at = models.DateTimeField(auto_now_add=True)
- approved = models.BooleanField(default=True)
-
- class Meta:
- ordering = ['created_at']
-
- def __str__(self):
- return f'Comment by {self.name} on {self.post}'
复制代码
3.2 数据库迁移
创建模型后,需要生成并应用迁移文件:
- # 生成迁移文件
- python manage.py makemigrations
- # 应用迁移
- python manage.py migrate
复制代码
3.3 注册模型到管理界面
为了在Django管理界面中管理模型,在blog/admin.py中注册它们:
- # blog/admin.py
- from django.contrib import admin
- from .models import Category, Tag, Post, Comment
- @admin.register(Category)
- class CategoryAdmin(admin.ModelAdmin):
- list_display = ['name', 'slug']
- prepopulated_fields = {'slug': ('name',)}
- @admin.register(Tag)
- class TagAdmin(admin.ModelAdmin):
- list_display = ['name', 'slug']
- prepopulated_fields = {'slug': ('name',)}
- @admin.register(Post)
- class PostAdmin(admin.ModelAdmin):
- list_display = ['title', 'author', 'status', 'created_at']
- list_filter = ['status', 'created_at', 'author']
- search_fields = ['title', 'content']
- prepopulated_fields = {'slug': ('title',)}
- raw_id_fields = ['author']
- date_hierarchy = 'created_at'
- ordering = ['status', '-created_at']
- filter_horizontal = ['categories', 'tags']
- @admin.register(Comment)
- class CommentAdmin(admin.ModelAdmin):
- list_display = ['name', 'post', 'created_at', 'approved']
- list_filter = ['approved', 'created_at']
- search_fields = ['name', 'email', 'content']
复制代码
3.4 创建超级用户
为了访问Django管理界面,需要创建一个超级用户:
- # 创建超级用户
- python manage.py createsuperuser
复制代码
按照提示输入用户名、邮箱和密码。
4. 模板创建
4.1 项目模板结构
在项目根目录下创建模板目录结构:
- myproject/
- templates/
- base.html
- blog/
- post_list.html
- post_detail.html
- post_form.html
- static/
- css/
- style.css
- js/
- main.js
- images/
复制代码
4.2 基础模板
创建templates/base.html作为基础模板:
4.3 文章列表模板
创建templates/blog/post_list.html:
- {% extends 'base.html' %}
- {% load static %}
- {% block title %}Blog - {{ block.super }}{% endblock %}
- {% block content %}
- <h1>Blog Posts</h1>
-
- {% for post in posts %}
- <article class="card mb-4">
- {% if post.featured_image %}
- <img src="{{ post.featured_image.url }}" class="card-img-top" alt="{{ post.title }}">
- {% endif %}
- <div class="card-body">
- <h2 class="card-title">
- <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
- </h2>
- <p class="text-muted">
- By {{ post.author }} on {{ post.created_at|date:"F d, Y" }}
- </p>
- <p class="card-text">{{ post.content|truncatewords:30 }}</p>
- <a href="{{ post.get_absolute_url }}" class="btn btn-primary">Read More →</a>
- </div>
- <div class="card-footer text-muted">
- <div class="d-flex justify-content-between">
- <div>
- Categories:
- {% for category in post.categories.all %}
- <a href="{% url 'blog:post_list_by_category' category.slug %}">{{ category.name }}</a>{% if not forloop.last %}, {% endif %}
- {% endfor %}
- </div>
- <div>
- Tags:
- {% for tag in post.tags.all %}
- <a href="{% url 'blog:post_list_by_tag' tag.slug %}">{{ tag.name }}</a>{% if not forloop.last %}, {% endif %}
- {% endfor %}
- </div>
- </div>
- </div>
- </article>
- {% empty %}
- <p>No posts found.</p>
- {% endfor %}
-
- <!-- Pagination -->
- {% if is_paginated %}
- <nav aria-label="Page navigation">
- <ul class="pagination justify-content-center">
- {% if page_obj.has_previous %}
- <li class="page-item">
- <a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">
- <span aria-hidden="true">«</span>
- </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 }}" aria-label="Next">
- <span aria-hidden="true">»</span>
- </a>
- </li>
- {% endif %}
- </ul>
- </nav>
- {% endif %}
- {% endblock %}
复制代码
4.4 文章详情模板
创建templates/blog/post_detail.html:
- {% extends 'base.html' %}
- {% load static %}
- {% block title %}{{ post.title }} - {{ block.super }}{% endblock %}
- {% block content %}
- <article class="card mb-4">
- {% if post.featured_image %}
- <img src="{{ post.featured_image.url }}" class="card-img-top" alt="{{ post.title }}">
- {% endif %}
- <div class="card-body">
- <h1 class="card-title">{{ post.title }}</h1>
- <p class="text-muted">
- By {{ post.author }} on {{ post.created_at|date:"F d, Y" }}
- </p>
- <div class="card-text">
- {{ post.content|linebreaks }}
- </div>
-
- <div class="mt-4">
- <div class="mb-2">
- <strong>Categories:</strong>
- {% for category in post.categories.all %}
- <a href="{% url 'blog:post_list_by_category' category.slug %}" class="badge badge-info">{{ category.name }}</a>
- {% endfor %}
- </div>
- <div>
- <strong>Tags:</strong>
- {% for tag in post.tags.all %}
- <a href="{% url 'blog:post_list_by_tag' tag.slug %}" class="badge badge-secondary">{{ tag.name }}</a>
- {% endfor %}
- </div>
- </div>
- </div>
- </article>
-
- <!-- Comments Section -->
- <div class="card mb-4">
- <div class="card-header">
- <h4>Comments ({{ post.comments.count }})</h4>
- </div>
- <div class="card-body">
- {% for comment in post.comments.all %}
- {% if comment.approved %}
- <div class="media mb-4">
- <div class="media-body">
- <h5 class="mt-0">{{ comment.name }}</h5>
- <p class="text-muted">{{ comment.created_at|date:"F d, Y, P" }}</p>
- <p>{{ comment.content|linebreaks }}</p>
- </div>
- </div>
- {% endif %}
- {% empty %}
- <p>No comments yet.</p>
- {% endfor %}
-
- <!-- Comment Form -->
- <hr>
- <h5>Leave a Comment</h5>
- <form method="post" action="{% url 'blog:add_comment' post.id %}">
- {% csrf_token %}
- <div class="form-group">
- <label for="id_name">Name</label>
- <input type="text" class="form-control" name="name" id="id_name" required>
- </div>
- <div class="form-group">
- <label for="id_email">Email</label>
- <input type="email" class="form-control" name="email" id="id_email" required>
- </div>
- <div class="form-group">
- <label for="id_content">Comment</label>
- <textarea class="form-control" name="content" id="id_content" rows="3" required></textarea>
- </div>
- <button type="submit" class="btn btn-primary">Submit</button>
- </form>
- </div>
- </div>
- {% endblock %}
复制代码
4.5 文章表单模板
创建templates/blog/post_form.html:
- {% extends 'base.html' %}
- {% load static %}
- {% block title %}{% if form.instance.pk %}Edit Post{% else %}New Post{% endif %} - {{ block.super }}{% endblock %}
- {% block content %}
- <h1>{% if form.instance.pk %}Edit Post{% else %}New Post{% endif %}</h1>
-
- <form method="post" enctype="multipart/form-data">
- {% csrf_token %}
- <div class="form-group">
- <label for="{{ form.title.id_for_label }}">Title</label>
- {{ form.title.errors }}
- <input type="text" class="form-control" name="{{ form.title.name }}" id="{{ form.title.id_for_label }}" value="{{ form.title.value|default:'' }}" required>
- </div>
-
- <div class="form-group">
- <label for="{{ form.slug.id_for_label }}">Slug</label>
- {{ form.slug.errors }}
- <input type="text" class="form-control" name="{{ form.slug.name }}" id="{{ form.slug.id_for_label }}" value="{{ form.slug.value|default:'' }}" required>
- </div>
-
- <div class="form-group">
- <label for="{{ form.content.id_for_label }}">Content</label>
- {{ form.content.errors }}
- <textarea class="form-control" name="{{ form.content.name }}" id="{{ form.content.id_for_label }}" rows="10" required>{{ form.content.value|default:'' }}</textarea>
- </div>
-
- <div class="form-group">
- <label for="{{ form.featured_image.id_for_label }}">Featured Image</label>
- {{ form.featured_image.errors }}
- <input type="file" class="form-control-file" name="{{ form.featured_image.name }}" id="{{ form.featured_image.id_for_label }}">
- {% if form.instance.featured_image %}
- <img src="{{ form.instance.featured_image.url }}" class="img-thumbnail mt-2" alt="{{ form.instance.title }}">
- {% endif %}
- </div>
-
- <div class="form-group">
- <label for="{{ form.status.id_for_label }}">Status</label>
- {{ form.status.errors }}
- <select class="form-control" name="{{ form.status.name }}" id="{{ form.status.id_for_label }}">
- {% for value, label in form.status.field.choices %}
- <option value="{{ value }}" {% if form.status.value == value %}selected{% endif %}>{{ label }}</option>
- {% endfor %}
- </select>
- </div>
-
- <div class="form-group">
- <label for="{{ form.categories.id_for_label }}">Categories</label>
- {{ form.categories.errors }}
- <select multiple class="form-control" name="{{ form.categories.name }}" id="{{ form.categories.id_for_label }}" size="5">
- {% for category in form.categories.field.queryset %}
- <option value="{{ category.id }}" {% if category.id in form.categories.value %}selected{% endif %}>{{ category.name }}</option>
- {% endfor %}
- </select>
- <small class="form-text text-muted">Hold Ctrl (or Cmd on Mac) to select multiple categories.</small>
- </div>
-
- <div class="form-group">
- <label for="{{ form.tags.id_for_label }}">Tags</label>
- {{ form.tags.errors }}
- <select multiple class="form-control" name="{{ form.tags.name }}" id="{{ form.tags.id_for_label }}" size="5">
- {% for tag in form.tags.field.queryset %}
- <option value="{{ tag.id }}" {% if tag.id in form.tags.value %}selected{% endif %}>{{ tag.name }}</option>
- {% endfor %}
- </select>
- <small class="form-text text-muted">Hold Ctrl (or Cmd on Mac) to select multiple tags.</small>
- </div>
-
- <button type="submit" class="btn btn-primary">Save</button>
- <a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
- </form>
- {% endblock %}
复制代码
5. 视图函数
5.1 基本视图
在blog/views.py中创建视图函数:
- # blog/views.py
- from django.shortcuts import render, get_object_or_404, redirect
- from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
- 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 django.db.models import Count
- from .models import Post, Category, Tag, Comment
- from .forms import PostForm, CommentForm
- class PostListView(ListView):
- model = Post
- template_name = 'blog/post_list.html'
- context_object_name = 'posts'
- paginate_by = 5
-
- def get_queryset(self):
- return Post.objects.filter(status='published')
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context['categories'] = Category.objects.all()
- context['tags'] = Tag.objects.all()
- return context
- class PostDetailView(DetailView):
- model = Post
- template_name = 'blog/post_detail.html'
- context_object_name = 'post'
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context['categories'] = Category.objects.all()
- context['tags'] = Tag.objects.all()
- 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
- return super().form_valid(form)
- class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
- model = Post
- form_class = PostForm
- template_name = 'blog/post_form.html'
-
- 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:post_list')
-
- def test_func(self):
- post = self.get_object()
- return self.request.user == post.author
- def post_list_by_category(request, category_slug):
- category = get_object_or_404(Category, slug=category_slug)
- posts = Post.objects.filter(status='published', categories=category)
-
- paginator = Paginator(posts, 5)
- page = request.GET.get('page')
-
- try:
- posts = paginator.page(page)
- except PageNotAnInteger:
- posts = paginator.page(1)
- except EmptyPage:
- posts = paginator.page(paginator.num_pages)
-
- categories = Category.objects.all()
- tags = Tag.objects.all()
-
- return render(request, 'blog/post_list.html', {
- 'posts': posts,
- 'category': category,
- 'categories': categories,
- 'tags': tags,
- 'is_paginated': True,
- 'page_obj': posts,
- })
- def post_list_by_tag(request, tag_slug):
- tag = get_object_or_404(Tag, slug=tag_slug)
- posts = Post.objects.filter(status='published', tags=tag)
-
- paginator = Paginator(posts, 5)
- page = request.GET.get('page')
-
- try:
- posts = paginator.page(page)
- except PageNotAnInteger:
- posts = paginator.page(1)
- except EmptyPage:
- posts = paginator.page(paginator.num_pages)
-
- categories = Category.objects.all()
- tags = Tag.objects.all()
-
- return render(request, 'blog/post_list.html', {
- 'posts': posts,
- 'tag': tag,
- 'categories': categories,
- 'tags': tags,
- 'is_paginated': True,
- 'page_obj': posts,
- })
- def add_comment(request, post_id):
- post = get_object_or_404(Post, id=post_id)
-
- if request.method == 'POST':
- name = request.POST.get('name')
- email = request.POST.get('email')
- content = request.POST.get('content')
-
- if name and email and content:
- comment = Comment(
- post=post,
- name=name,
- email=email,
- content=content
- )
- comment.save()
-
- return redirect('blog:post_detail', pk=post.id)
复制代码
5.2 表单类
在blog目录下创建forms.py文件:
- # blog/forms.py
- from django import forms
- from .models import Post, Comment
- class PostForm(forms.ModelForm):
- class Meta:
- model = Post
- fields = ['title', 'slug', 'content', 'featured_image', 'status', 'categories', 'tags']
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields['categories'].widget.attrs.update({'size': '5'})
- self.fields['tags'].widget.attrs.update({'size': '5'})
- class CommentForm(forms.ModelForm):
- class Meta:
- model = Comment
- fields = ['name', 'email', 'content']
复制代码
5.3 URL配置
在blog目录下创建urls.py文件:
- # blog/urls.py
- from django.urls import path
- from . import views
- app_name = 'blog'
- urlpatterns = [
- path('', views.PostListView.as_view(), name='post_list'),
- path('post/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),
- path('post/new/', views.PostCreateView.as_view(), name='post_create'),
- path('post/<int:pk>/edit/', views.PostUpdateView.as_view(), name='post_update'),
- path('post/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
- path('category/<slug:category_slug>/', views.post_list_by_category, name='post_list_by_category'),
- path('tag/<slug:tag_slug>/', views.post_list_by_tag, name='post_list_by_tag'),
- path('post/<int:post_id>/comment/', views.add_comment, name='add_comment'),
- ]
复制代码
5.4 项目URL配置
更新项目的urls.py文件:
- # myproject/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')),
- ]
- if settings.DEBUG:
- urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
复制代码
6. 高级功能
6.1 搜索功能
在blog/views.py中添加搜索视图:
- # blog/views.py
- from django.db.models import Q
- def search(request):
- query = request.GET.get('q')
- posts = Post.objects.filter(status='published')
-
- if query:
- posts = posts.filter(
- Q(title__icontains=query) |
- Q(content__icontains=query) |
- Q(categories__name__icontains=query) |
- Q(tags__name__icontains=query)
- ).distinct()
-
- paginator = Paginator(posts, 5)
- page = request.GET.get('page')
-
- try:
- posts = paginator.page(page)
- except PageNotAnInteger:
- posts = paginator.page(1)
- except EmptyPage:
- posts = paginator.page(paginator.num_pages)
-
- categories = Category.objects.all()
- tags = Tag.objects.all()
-
- return render(request, 'blog/post_list.html', {
- 'posts': posts,
- 'query': query,
- 'categories': categories,
- 'tags': tags,
- 'is_paginated': True,
- 'page_obj': posts,
- })
复制代码
在blog/urls.py中添加搜索URL:
- # blog/urls.py
- urlpatterns = [
- # ... 其他URL
- path('search/', views.search, name='search'),
- ]
复制代码
在基础模板中添加搜索表单:
- <!-- 在base.html的navbar中添加 -->
- <form class="form-inline my-2 my-lg-0" action="{% url 'blog:search' %}" method="get">
- <input class="form-control mr-sm-2" type="search" name="q" placeholder="Search" aria-label="Search">
- <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
- </form>
复制代码
6.2 RSS订阅
在blog目录下创建feeds.py文件:
- # blog/feeds.py
- from django.contrib.syndication.views import Feed
- from django.urls import reverse
- from django.utils.feedgenerator import Rss201rev2Feed
- from .models import Post
- class LatestPostsFeed(Feed):
- feed_type = Rss201rev2Feed
- title = "My Blog"
- link = "/blog/"
- description = "Latest posts from My Blog"
-
- def items(self):
- return Post.objects.filter(status='published')[:10]
-
- def item_title(self, item):
- return item.title
-
- def item_description(self, item):
- return item.content[:200] + "..."
-
- def item_link(self, item):
- return reverse('blog:post_detail', args=[item.pk])
-
- def item_pubdate(self, item):
- return item.created_at
复制代码
在blog/urls.py中添加RSS订阅URL:
- # blog/urls.py
- from django.urls import path
- from . import views
- from .feeds import LatestPostsFeed
- app_name = 'blog'
- urlpatterns = [
- # ... 其他URL
- path('feed/', LatestPostsFeed(), name='post_feed'),
- ]
复制代码
在基础模板中添加RSS链接:
- <!-- 在base.html的head部分添加 -->
- <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="{% url 'blog:post_feed' %}">
复制代码
6.3 站点地图
在blog目录下创建sitemaps.py文件:
- # blog/sitemaps.py
- from django.contrib.sitemaps import Sitemap
- from django.urls import reverse
- from .models import Post, Category, Tag
- class PostSitemap(Sitemap):
- changefreq = "weekly"
- priority = 0.9
-
- def items(self):
- return Post.objects.filter(status='published')
-
- def lastmod(self, obj):
- return obj.updated_at
-
- def location(self, obj):
- return reverse('blog:post_detail', args=[obj.pk])
- class CategorySitemap(Sitemap):
- changefreq = "monthly"
- priority = 0.7
-
- def items(self):
- return Category.objects.all()
-
- def location(self, obj):
- return reverse('blog:post_list_by_category', args=[obj.slug])
- class TagSitemap(Sitemap):
- changefreq = "monthly"
- priority = 0.6
-
- def items(self):
- return Tag.objects.all()
-
- def location(self, obj):
- return reverse('blog:post_list_by_tag', args=[obj.slug])
- class StaticViewSitemap(Sitemap):
- priority = 0.5
- changefreq = 'monthly'
-
- def items(self):
- return ['blog:post_list']
-
- def location(self, item):
- return reverse(item)
复制代码
在项目的urls.py中添加站点地图URL:
- # myproject/urls.py
- from django.contrib import admin
- from django.urls import path, include
- from django.contrib.sitemaps.views import sitemap
- from blog.sitemaps import PostSitemap, CategorySitemap, TagSitemap, StaticViewSitemap
- sitemaps = {
- 'posts': PostSitemap,
- 'categories': CategorySitemap,
- 'tags': TagSitemap,
- 'static': StaticViewSitemap,
- }
- urlpatterns = [
- path('admin/', admin.site.urls),
- path('', include('blog.urls')),
- path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
- ]
复制代码
7. 测试
7.1 模型测试
在blog/tests.py中编写模型测试:
- # blog/tests.py
- from django.test import TestCase
- from django.contrib.auth.models import User
- from .models import Post, Category, Tag, Comment
- class CategoryModelTest(TestCase):
- def setUp(self):
- self.category = Category.objects.create(
- name="Test Category",
- slug="test-category",
- description="Test category description"
- )
-
- def test_category_creation(self):
- self.assertEqual(self.category.name, "Test Category")
- self.assertEqual(self.category.slug, "test-category")
- self.assertEqual(self.category.description, "Test category description")
- self.assertEqual(str(self.category), "Test Category")
- class TagModelTest(TestCase):
- def setUp(self):
- self.tag = Tag.objects.create(
- name="Test Tag",
- slug="test-tag"
- )
-
- def test_tag_creation(self):
- self.assertEqual(self.tag.name, "Test Tag")
- self.assertEqual(self.tag.slug, "test-tag")
- self.assertEqual(str(self.tag), "Test Tag")
- class PostModelTest(TestCase):
- def setUp(self):
- self.user = User.objects.create_user(
- username='testuser',
- email='test@example.com',
- password='testpass123'
- )
- self.category = Category.objects.create(
- name="Test Category",
- slug="test-category"
- )
- self.tag = Tag.objects.create(
- name="Test Tag",
- slug="test-tag"
- )
- self.post = Post.objects.create(
- title="Test Post",
- slug="test-post",
- author=self.user,
- content="This is a test post content.",
- status="published"
- )
- self.post.categories.add(self.category)
- self.post.tags.add(self.tag)
-
- def test_post_creation(self):
- self.assertEqual(self.post.title, "Test Post")
- self.assertEqual(self.post.slug, "test-post")
- self.assertEqual(self.post.author, self.user)
- self.assertEqual(self.post.content, "This is a test post content.")
- self.assertEqual(self.post.status, "published")
- self.assertEqual(str(self.post), "Test Post")
- self.assertIn(self.category, self.post.categories.all())
- self.assertIn(self.tag, self.post.tags.all())
- class CommentModelTest(TestCase):
- def setUp(self):
- self.user = User.objects.create_user(
- username='testuser',
- email='test@example.com',
- password='testpass123'
- )
- self.post = Post.objects.create(
- title="Test Post",
- slug="test-post",
- author=self.user,
- content="This is a test post content.",
- status="published"
- )
- self.comment = Comment.objects.create(
- post=self.post,
- name="Test Commenter",
- email="commenter@example.com",
- content="This is a test comment."
- )
-
- def test_comment_creation(self):
- self.assertEqual(self.comment.post, self.post)
- self.assertEqual(self.comment.name, "Test Commenter")
- self.assertEqual(self.comment.email, "commenter@example.com")
- self.assertEqual(self.comment.content, "This is a test comment.")
- self.assertTrue(self.comment.approved)
- self.assertEqual(str(self.comment), 'Comment by Test Commenter on Test Post')
复制代码
7.2 视图测试
继续在blog/tests.py中添加视图测试:
- # blog/tests.py (继续)
- from django.test import Client
- from django.urls import reverse
- class PostListViewTest(TestCase):
- def setUp(self):
- self.client = Client()
- self.user = User.objects.create_user(
- username='testuser',
- email='test@example.com',
- password='testpass123'
- )
- self.category = Category.objects.create(
- name="Test Category",
- slug="test-category"
- )
- self.tag = Tag.objects.create(
- name="Test Tag",
- slug="test-tag"
- )
- # Create 10 published posts
- for i in range(10):
- Post.objects.create(
- title=f"Test Post {i}",
- slug=f"test-post-{i}",
- author=self.user,
- content=f"This is test post {i} content.",
- status="published"
- )
- # Create 2 draft posts
- for i in range(2):
- Post.objects.create(
- title=f"Draft Post {i}",
- slug=f"draft-post-{i}",
- author=self.user,
- content=f"This is draft post {i} content.",
- status="draft"
- )
-
- def test_view_url_exists_at_desired_location(self):
- response = self.client.get('')
- self.assertEqual(response.status_code, 200)
-
- def test_view_url_accessible_by_name(self):
- response = self.client.get(reverse('blog:post_list'))
- self.assertEqual(response.status_code, 200)
-
- def test_view_uses_correct_template(self):
- response = self.client.get(reverse('blog:post_list'))
- self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'blog/post_list.html')
-
- def test_pagination_is_five(self):
- response = self.client.get(reverse('blog:post_list'))
- self.assertEqual(response.status_code, 200)
- self.assertTrue('is_paginated' in response.context)
- self.assertTrue(response.context['is_paginated'] == True)
- self.assertEqual(len(response.context['posts']), 5)
-
- def test_lists_all_posts(self):
- # Get second page and confirm it has (exactly) remaining 5 items
- response = self.client.get(reverse('blog:post_list') + '?page=2')
- self.assertEqual(response.status_code, 200)
- self.assertTrue('is_paginated' in response.context)
- self.assertTrue(response.context['is_paginated'] == True)
- self.assertEqual(len(response.context['posts']), 5)
-
- def test_only_published_posts(self):
- response = self.client.get(reverse('blog:post_list'))
- self.assertEqual(response.status_code, 200)
- self.assertEqual(len(response.context['posts']), 5)
- for post in response.context['posts']:
- self.assertEqual(post.status, 'published')
- class PostDetailViewTest(TestCase):
- def setUp(self):
- self.client = Client()
- self.user = User.objects.create_user(
- username='testuser',
- email='test@example.com',
- password='testpass123'
- )
- self.post = Post.objects.create(
- title="Test Post",
- slug="test-post",
- author=self.user,
- content="This is a test post content.",
- status="published"
- )
-
- def test_view_url_exists_at_desired_location(self):
- response = self.client.get(f'/post/{self.post.pk}/')
- self.assertEqual(response.status_code, 200)
-
- def test_view_url_accessible_by_name(self):
- response = self.client.get(reverse('blog:post_detail', kwargs={'pk': self.post.pk}))
- self.assertEqual(response.status_code, 200)
-
- def test_view_uses_correct_template(self):
- response = self.client.get(reverse('blog:post_detail', kwargs={'pk': self.post.pk}))
- self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'blog/post_detail.html')
-
- def test_nonexistent_post(self):
- response = self.client.get(reverse('blog:post_detail', kwargs={'pk': 999}))
- self.assertEqual(response.status_code, 404)
- class PostCreateViewTest(TestCase):
- def setUp(self):
- self.client = Client()
- self.user = User.objects.create_user(
- username='testuser',
- email='test@example.com',
- password='testpass123'
- )
- self.category = Category.objects.create(
- name="Test Category",
- slug="test-category"
- )
- self.tag = Tag.objects.create(
- name="Test Tag",
- slug="test-tag"
- )
-
- def test_redirect_if_not_logged_in(self):
- response = self.client.get(reverse('blog:post_create'))
- self.assertRedirects(response, '/accounts/login/?next=/post/new/')
-
- def test_logged_in_uses_correct_template(self):
- self.client.login(username='testuser', password='testpass123')
- response = self.client.get(reverse('blog:post_create'))
- self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'blog/post_form.html')
-
- def test_create_post(self):
- self.client.login(username='testuser', password='testpass123')
- response = self.client.post(reverse('blog:post_create'), {
- 'title': 'New Test Post',
- 'slug': 'new-test-post',
- 'content': 'This is a new test post content.',
- 'status': 'published',
- 'categories': [self.category.id],
- 'tags': [self.tag.id]
- })
- self.assertEqual(response.status_code, 302)
- self.assertTrue(Post.objects.filter(title='New Test Post').exists())
- new_post = Post.objects.get(title='New Test Post')
- self.assertEqual(new_post.author, self.user)
- self.assertIn(self.category, new_post.categories.all())
- self.assertIn(self.tag, new_post.tags.all())
复制代码
7.3 运行测试
运行测试以验证代码的正确性:
- # 运行所有测试
- python manage.py test
- # 运行特定应用的测试
- python manage.py test blog
- # 运行特定测试类
- python manage.py test blog.tests.PostModelTest
- # 运行特定测试方法
- python manage.py test blog.tests.PostModelTest.test_post_creation
复制代码
8. 部署上线
8.1 生产环境配置
创建生产环境设置文件myproject/settings_production.py:
- # myproject/settings_production.py
- from .settings import *
- # Security settings
- DEBUG = False
- ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
- # Database settings (using PostgreSQL as an example)
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.postgresql',
- 'NAME': 'mydatabase',
- 'USER': 'mydatabaseuser',
- 'PASSWORD': 'mypassword',
- 'HOST': 'localhost',
- 'PORT': '5432',
- }
- }
- # Static files settings
- STATIC_ROOT = '/var/www/myproject/static/'
- MEDIA_ROOT = '/var/www/myproject/media/'
- # Security settings
- SECURE_SSL_REDIRECT = True
- SESSION_COOKIE_SECURE = True
- CSRF_COOKIE_SECURE = True
- SECURE_BROWSER_XSS_FILTER = True
- SECURE_CONTENT_TYPE_NOSNIFF = True
- X_FRAME_OPTIONS = 'DENY'
- # Email settings
- EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
- EMAIL_HOST = 'smtp.yourdomain.com'
- EMAIL_PORT = 587
- EMAIL_USE_TLS = True
- EMAIL_HOST_USER = 'your_email@yourdomain.com'
- EMAIL_HOST_PASSWORD = 'your_email_password'
- DEFAULT_FROM_EMAIL = 'your_email@yourdomain.com'
- # Logging settings
- LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': False,
- 'handlers': {
- 'file': {
- 'level': 'ERROR',
- 'class': 'logging.FileHandler',
- 'filename': '/var/log/django/error.log',
- },
- },
- 'loggers': {
- 'django': {
- 'handlers': ['file'],
- 'level': 'ERROR',
- 'propagate': True,
- },
- },
- }
复制代码
8.2 收集静态文件
在部署前,收集所有静态文件到一个目录:
- # 设置环境变量
- export DJANGO_SETTINGS_MODULE=myproject.settings_production
- # 收集静态文件
- python manage.py collectstatic
复制代码
8.3 服务器配置
- # 更新系统
- sudo apt update
- sudo apt upgrade
- # 安装Python和pip
- sudo apt install python3 python3-pip python3-venv
- # 安装PostgreSQL
- sudo apt install postgresql postgresql-contrib
- # 安装Nginx
- sudo apt install nginx
- # 安装Gunicorn
- pip install gunicorn
复制代码- # 切换到PostgreSQL用户
- sudo -u postgres psql
- # 创建数据库和用户
- CREATE DATABASE mydatabase;
- CREATE USER mydatabaseuser WITH PASSWORD 'mypassword';
- ALTER ROLE mydatabaseuser SET client_encoding TO 'utf8';
- ALTER ROLE mydatabaseuser SET default_transaction_isolation TO 'read committed';
- ALTER ROLE mydatabaseuser SET timezone TO 'UTC';
- GRANT ALL PRIVILEGES ON DATABASE mydatabase TO mydatabaseuser;
- \q
复制代码
创建Gunicorn服务文件/etc/systemd/system/gunicorn.service:
- [Unit]
- Description=gunicorn daemon
- After=network.target
- [Service]
- User=www-data
- Group=www-data
- WorkingDirectory=/var/www/myproject
- ExecStart=/var/www/myproject/venv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock myproject.wsgi:application
- [Install]
- WantedBy=multi-user.target
复制代码
启动Gunicorn服务:
- # 启动Gunicorn服务
- sudo systemctl start gunicorn
- sudo systemctl enable gunicorn
- # 检查服务状态
- sudo systemctl status gunicorn
复制代码
创建Nginx配置文件/etc/nginx/sites-available/myproject:
- server {
- listen 80;
- server_name yourdomain.com www.yourdomain.com;
- location = /favicon.ico { access_log off; log_not_found off; }
- location /static/ {
- root /var/www/myproject;
- }
- location /media/ {
- root /var/www/myproject;
- }
- location / {
- include proxy_params;
- proxy_pass http://unix:/run/gunicorn.sock;
- }
- }
复制代码
启用配置并重启Nginx:
- # 启用站点
- sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/
- # 测试Nginx配置
- sudo nginx -t
- # 重启Nginx
- sudo systemctl restart nginx
复制代码
8.4 SSL证书配置
使用Let’s Encrypt获取SSL证书:
- # 安装Certbot
- sudo apt install certbot python3-certbot-nginx
- # 获取SSL证书
- sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
- # 测试自动续期
- sudo certbot renew --dry-run
复制代码
8.5 部署流程
创建部署脚本deploy.sh:
- #!/bin/bash
- # Variables
- PROJECT_DIR="/var/www/myproject"
- VENV_DIR="$PROJECT_DIR/venv"
- SERVICE_NAME="gunicorn"
- NGINX_SITES="/etc/nginx/sites-available"
- # Pull latest changes
- cd $PROJECT_DIR
- git pull origin main
- # Activate virtual environment
- source $VENV_DIR/bin/activate
- # Install dependencies
- pip install -r requirements.txt
- # Run migrations
- python manage.py migrate --settings=myproject.settings_production
- # Collect static files
- python manage.py collectstatic --noinput --settings=myproject.settings_production
- # Restart Gunicorn
- sudo systemctl restart $SERVICE_NAME
- # Restart Nginx
- sudo systemctl restart nginx
- echo "Deployment completed successfully!"
复制代码
使脚本可执行并运行:
- chmod +x deploy.sh
- ./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开发之旅中取得成功! |
|