博客系统开发-1

博客系统开发

创建博客系统

开发环境初步设置

本项目开发环境:python 3.8,Django 2.1.4。本章开发的博客系统涉及图片上传与存储、文章发布等功能,因此需要安装富文本编辑器和图形模块。

安装django-ckeditor

在博客系统发表的文章一般需要各种排版样式,文章发布者不可能用HTML语法给文章增加格式,因此需要一个富文本编辑器提供类似Microsoft Word的编辑功能,让发布博客文章的用户不用编写HTML代码也可以设置各种文本格式。django-ckeditor是一款功能较全的富文本编辑器,与Django无缝衔接,直接用pip安装即可。

安装pillow

pillow提供了基本的图像处理功能,如改变图像大小、旋转图像、图像格式转换、图像增强、直方图处理、插值和滤波等,命令行直接安装即可。

创建项目

切换到要放项目的文件夹,终端输入:

django-admin startproject test_blog
cd test_blog
python manage.py startapp blog

注册博客应用程序

在settings中的INSTALLED_APPS加入blog。

数据库选择

选择django内置的数据库SQLite3.它是一个轻量级的数据库,仅有一个文件,方便移动和复制,非常有利于开发调试。开发人员可以先用此数据库进行程序设计与调试,等程序正确无误再切换到MySQL等数据库。

博客系统应用程序开发

项目数据库表结构设计

博客系统主要用于发布文章,因此数据库表不多,有Category、Tag、Post、loguser,在models中编写Category和Tag:

from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=32,verbose_name='分类名')
    des = models.CharField(max_length=100,verbose_name='备注',null=True)
    """
    在__str__(self)函数中返回的值是这个数据模型类的实例对象表述,
    可理解为数据库表的一条记录对象的别名
    如果print()函数的参数为该数据模型对象实例(记录对象)时,会输出值self.name
    Django Admin管理后台默认列表页面的每一条记录,会显示这个函数的返回值self.name
    """
    def __str__(self):
        return self.name
    """
    Meta类封装了一些数据库的信息
    如verbose_name指定Django Admin后台管理中数据库表名的单复数
    db_table可自定义数据库表名,不用默认名称
    Django默认用app_类命名数据库表,如blog_category
    index_together联合索引,unique_together联合唯一索引
    ordering指定默认排序字段
    """
    class Meta:
        verbose_name='分类'
        verbose_name_plural='分类'

class Tag(models.Model):
    name = models.CharField(max_length=32,verbose_name='标签名')
    des = models.CharField(max_length=100,verbose_name='备注',null=True)
    def __str__(self):
        return self.name
    class Meta:
        verbose_name='标签'
        verbose_name_plural='标签'

下面是Post表的语句:

from django.urls import reverse
# 调用富文本编辑相关模块
from ckeditor_uploader.fields import RichTextUploadingField
# 导入strip_tags()函数,代码中用这个函数截取字段中的字符串
from django.utils.html import strip_tags
class Blog(models.Model):
    # 文章标题
    title = models.CharField(max_length=70,verbose_name='文章标题')
    """
    文章正文,存放大量文本、格式、图片地址等内容,需排版
    所以引用RichTextUploadingField调用富文本编辑器
    """
    body = RichTextUploadingField(verbose_name='文本内容')
    # 文章的创建时间
    created_time = models.DateTimeField(verbose_name='创建时间')
    # 文章的最后一次修改时间
    modified_time = models.DateTimeField(verbose_name='修改时间')
    """
    excerpt 字段存储文章的摘要
    CharField类型字段默认不能为空,这里文章摘要可以为空
    所以要指定blank=True就允许空值了
    """
    excerpt = models.CharField(max_length=200,blank=True,verbose_name='文章摘要')
    """
    category是设置博客文章分类的字段,与前面定义的category是多对一关系
    即多个博客文章记录可归于一个类别
    """
    category = models.ForeignKey(Category,on_delete=models.CASCADE,verbose_name='分类')
    """
    tags是标签字段,一篇博客文章可以有多个标签,一个标签下可能有多篇文章
    blank=True允许文章没有标签
    """
    tags = models.ManyToManyField(Tag,blank=True,verbose_name='标签')
    # author为博客文章作者,文章作者我们用的是loguser表中定义的用户
    # 这里用外键与该表关联
    author = models.ForeignKey(loguser,on_delete=models.CASCADE,verbose_name='作者')
    # 记录博客文章阅读量,起始值设为0
    # 后面代码为这个字段定义一个increase_views函数,文章每被查看一次,该字段值加1
    views = models.IntegerField(default=0,verbose_name='查看次数')
    def get_absolute_url(self):
        return reverse('blog:detail',kwargs={'pk':self.pk})
    # increase_views()把views字段值加1,然后调用save方法将更改后的值保存到数据库
    def increase_views(self):
        self.views += 1
        self.save(update_fields=['views'])
    # save函数是数据模型类的方法,我们重写这个方法是为了自动提取摘要内容
    def save(self,*args,**kwargs):
        # 如果没有填写博客文章的摘要
        if not self.excerpt:
            """
            由于博客文章是由富文本编辑器写的,文件中带有大量HTML标签
            用strip()函数可能会把HTML标签截断
            strip_tags()会把字段中的HTML标签删去,然后在纯文本中截取字符串
            """
            self.excerpt = strip_tags(self.body)[:118]
            # 调用父类的save方法将数据保存到数据库中
            super(Blog,self).save(*args,**kwargs)
        else:
            # 重写save必须调用父类的save方法,否则数据不会保存到数据库
            super(Blog, self).save(*args,**kwargs)
    def __str__(self):
        return self.title
    class Meta:
        # 设置按created_time的值倒序排列,这样最新的博客文章排在前面
        ordering = ['-created_time']
        verbose_name='文档管理表'
        verbose_name_plural='文档管理表'

说明:

  1. 字段category存储博客文章类别,其值是ForeignKey类型,这样指定一条博客文章记录只能属于一个分类,一条类别记录可以有多条博客文章记录相对应。数据模型类中有ForeignKey属性的字段在数据库表中的字段名为“属性名_id”,category在数据库表中的字段名是category_id。该字段与数据库表Category中的主键id字段是多对一的关联关系。
  2. 字段tags存储博客文章标签,是多对多键,一条博客文章记录可以对应多条标签记录,一条标签记录可以对应多条博客文章记录。
  3. 在一个数据模型中新增一个多对多键,在生成数据库表时,并没有在数据库表中生成这个字段,而是额外生成一张数据库表把有多对多关系的两个表关联起来,表的命名格式为“应用程序名_数据模型名_多对多键字段名”,表名都是小写,本项目中表名为blog_blog_tags。这个表有3个字段。分别为主键id和两个外键,两个外键关联两个多对多关系的数据库表,两个外键字段命名格式为“数据模型名_id”。本项目中字段名为blog_id和tag_id。
  4. 字段author存储博客文章作者的名字,这个字段是外键类型,与loguser数据库表形成多对一关系,指明了一篇文章只能有一个作者,一个作者可写多篇文章。
  5. 数据模型中get_absolute_url()方法主要作用是返回一个url,调用redirect(obj)函数时,如果这个obj是这个数据模型实例对象(相当于一条表记录),redirect执行后重定向到该obj对象的get_absolute_url()方法返回的URL。
  6. get_absolute_url()方法中用到的reverse()函数是一个URL反向解析函数。解析过程如下:
    • 在reverse('blog:detail',kwargs={'pk':self.pk})中的第一个参数的值是'blog:detail',意思是在blog应用下name=detail的URL配置项。
    • 在配置文件urls.py中有语句app_name='blog',这设定了这个URL配置项是属于blog应用程序。配置项re_path('blog/(?P<pk>[0-9]+)/',views.blogdetailview.as_view(),name='detail'),设定配置项的name='detail'。reverse函数会找到这个配置项,并把URL表达式也就是blog/(?P<pk>[0-9]+)/中的参数pk替换。如果Blog数据模型的实例对象的id或pk(主键,与id是同一个字段)是18的话,那么reverse函数会解析为/blog/18/。
  7. increase_views是一个自定义方法,Blog类的实例对象可以通过调用该方法,将该对象的views字段的值加1,最后调用save函数保存到数据库表。方法中的save函数使用update_fields参数限制Django只更新数据库表中views字段的值。

数据模型loguser中存储的用户成为Django Admin管理用户,可以登录管理后台、管理项目的数据。也就是说表中存储的用户具有Django系统内置的User对象的权限与功能。实现上述需求可以通过继承的方式拥有User模型的所有属性:

# Django用户认证系统提供了一个内置的User对象,我们想通过扩展这个用户以增加新字段,扩展方式可以通过
# 继承AbstractUser的方式。
from django.contrib.auth.models import AbstractUser
class loguser(AbstractUser):
    # 增加一个nikename字段用来存储用户的名字,我们在博客相关网页上显示这个名字
    nikename = models.CharField(max_length=32,verbose_name='昵称',blank=True)
    telephone = models.CharField(max_length=11,null=True,unique=True)
    """
    head_img存储用户头像,在数据库表中存储的是文件的相对地址
    字段值形式为upload_to的值/filename
    图片文件实际地址有settings中的MEDIA_ROOT和head_img中的upload_to决定
    """
    head_img = models.ImageField(upload_to='headimage',blank=True,null=True,verbose_name='头像')
    def __str__(self):
        return self.username
    class Meta:
        verbose_name='用户信息表'
        verbose_name_plural='用户信息表'

说明:

  1. Django内置的User数据模型包含username、password、email、first_name、last_name等属性,这里我们建立loguser数据模型类,增加了nikename、telephone、head_img属性。
  2. 数据模型loguser需要继承AbstractUser才能成为认证系统的用户模型。

建立loguser后如果要让Django用户认证系统使用我们自定义的用户模型,而不再使用内置User数据模型,需要通过settings中的AUTH_USER_MODEL指定,所以加入AUTH_USER_MODEL="blog.loguser"。

我们设计的博客系统中,用loguser管理系统用户,并把博客文章作者设置为loguser表中用户,通过外键为博客表与loguser表建立关系。

CKEditor富文本编辑器相关知识介绍

安装

pip install django-ckeditor

pip install pillow

注册富文本编辑器

在settings中的INSTALLED_APPS代码块中加入ckeditor和ckeditor_uploader(可支持图片上传)。

配置富文本编辑器

在settings中增加以下代码:

# 指定富文本编辑器或其他上传文件的根目录,这里为/test_blog/media/
MEDIA_ROOT=os.path.join(BASE_DIR,'media')
# MEDIA_URL指定上传图片的路径前缀字符串,即在模板文件中遇到前缀为/media/的URL路径会解析为/test_blog/media/
MEDIA_URL='/media/'
# CKEDITOR_UPLOAD_PATH设置富文本编辑器的上传文件的相对路径
# 它与MEDIA_ROOT组成完整的路径,即/test_blog/media/upload/
CKEDITOR_UPLOAD_PATH='upload'
# 设置图片处理的引擎为pillow,用于生成图片缩略图,在编辑器里浏览上传的图片
CKEDITOR_IMAGE_BACKEND='pillow'

配置URL

在urls中进行修改,首先通过path('ckeditor/',include('ckeditor_uploader.urls'))引入ckeditor的URL配置文件到项目中。

from django.contrib import admin
from django.urls import path,include
from django.conf.urls.static import static
from . import settings
urlpatterns = [
    path('admin/', admin.site.urls),
    path('ckeditor/',include('ckeditor_uploader.urls')),
] + static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT) #没有这一行将无法显示上传的图片

上传的图片要存储到media中,因此需要设置media可被访问。在调试模式下增加最后一行代码让django取得MEDIA_ROOT指向的路径。建立MEDIA_URL与MEDIA_ROOT的对应关系,使静态模块为指定静态文件夹提供服务。

其他可选配置

在settings中增加以下几项,对富文本编辑器进行配置:

  • CKEDITOR_BROWSE_SHOW_DIRS = True,在编辑器里浏览上传的图片时,图片会以路径分组、以日期排序。
  • CKEDITOR_ALLOW_NONIMAGE_FILES=False,不允许非图片文件上传,默认为true
  • CKEDITOR_RESTRICT_BY_USER=True,限制用户浏览图片的权限,只能浏览自己上传的图片,图片会传到以用户名命名的文件夹下,但超级用户能查看所有图片。
  • 如果想自定义编辑器,添加或删除一些按钮,需要在settings中设置如下:
CKEDITOR_CONFIGS = {
    # 配置名为default时,django-ckeditor默认使用这个配置
    'default':{
        # 使用简体中文
        'language':'zh-cn',
        # 设置富文本编辑器的高度和宽度
        'width':'600px',
        'height':'200px',
        # 设置工具栏为自定义,名字为Custom
        'toolbar':'Custom',
        # 添加富文本编辑器的工具栏上的按钮
        'toolbar_Custom':[
            ['Bold','Italic','Underline'],
            ['NumberedList','BulletedList'],
            ['Image','Link','Unlink'],
            ['Maximize']]}
    # 设置另一个django-ckeditor配置,名为test
    'test':{
        
    }
}

使用非默认配置时,需要在RichTextUploadingField()里指定该配置名称,代码如下:

class Blog(models.Model):
    # 编辑器使用test配置
    body=RichTextUploadingField(config_name='test',verbose_name='文本内容')

Django CKEditor默认只允许Django系统用户(Django Admin管理后台中的登录用户)具有图片上传权限,因此使用图片上传功能需要先登录。还有就是django-ckeditor富文本编辑器一般只在Django Admin管理后台的页面上使用,如果在非管理后台使用要引入相应的JavaScript文件。

非Django Admin后台页面使用django-ckeditor

django-ckeditor富文本编辑器可以用在非Django Admin管理后台页面(以下简称为前端页面),但是默认无图片上传功能,因为django-ckeditor默认只有Django Admin管理后台登录用户才有图片上传权限。要想在前端页面使用富文本编辑器并具有图片上传功能,可以经过两步,登录后台或者通过代码让系统用户处于登录状态,然后定向到这个前端页面。下面举例说明。

首先在blog应用下新建的urls中加入一个配置项:

from django.urls import path,include,re_path
from blog import views
app_name = 'blog'
urlpatterns = [
    ...
    path('test_ckeditor_front/',views.test_ckeditor_front),
]

这个URL配置文件是二级配置,一级配置为path('',include('blog.urls')),因此这个匹配项的URL完整的路径为/test_ckeditor_front/。

然后编写视图函数test_ckeditor_font():

from django.shortcuts import render
from . import models
# 导入Django的认证模块,因为代码中要模拟用户登录
from django.contrib.auth.models import auth
def test_ckeditor_front(request):
    # 从loguser中取出第一条记录,loguser继承AbstractUser
    # 也就是说loguser的成员是系统用户,为了测试取出第一条记录
    user_obj = models.loguser.objects.all().first()
    # 通过认证模块让用户处于登录状态
    auth.login(request,user_obj)
    # 取出第一条测试数据
    blog = models.Blog.objects.get(id=1)
    # 把数据传递给页面
    return render(request,'blog/test_ckeditor_front.html',{'blog':blog})

说明:

  1. 根据django-ckeditor用法,为了实现前端页面上编辑器有图片上传权限,我们利用Django认证模块auth中的login()函数模拟一个系统用户登录。
  2. 数据模型Blog中的body字段是RichTextUploadingField类型,我们利用这个字段测试富文本编辑器。

test_ckeditor_front.html如下:

{% load staticfiles %}
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试前台页面使用Django-CKEditor</title>
    <!--引入Bootstrap样式-->
    <link href="{% static 'blog/css/bootstrap.min.css' %}" rel="stylesheet">
    <!--导入ckeditor的JS脚本,src的值是默认路径-->
    <script src="{% static 'ckeditor/ckeditor/ckeditor.js' %}"></script>
    <!--导入ckeditor的初始化JS脚本,src的值是默认路径-->
    <script  src="{% static "ckeditor/ckeditor-init.js" %}"></script>

</head>
<body>
<div class="row">
    <div class="col-md-offset-2 col-md-8">
        <!--form标签中的enctype属性设为multipart/form-data才能实现图片上传-->
        <form novalidate action="" method="post" class="form-horizontal" enctype="multipart/form-data">
            {% csrf_token %}
            <div class="form-group">
                <label for="title" class="col-sm-2 control-label">文章标题</label>
                <div class="col-sm-10">
                    <input type="text" class="form-control" id="title" name="title" value="{{ blog.title }}">
                </div>
            </div>
            <div class="form-group">
                <label for="body" class="col-sm-2 control-label">文章内容</label>
                <div class="col-sm-10">
                    <!-- 用textarea标签接收blog.body变量,转生成富文本编辑器-->
                    <textarea name="body">{{ blog.body }}</textarea>
                    <script>
                       用脚本把textare标签转换为CKEditor富文本编辑控件,通过参数设置富文本编辑器的外形、上传图片处理功能、图片浏览功能。
                       CKEDITOR.replace( 'body',  {width: '860px',height:'600px',
                       filebrowserBrowseUrl: '/ckeditor/browse/' ,
                       filebrowserUploadUrl: '/ckeditor/upload/'});
                    </script>
                </div>
            </div>

        </form>
    </div>
</div>
</body>

<script src="{% static 'blog/js/jquery-2.1.3.min.js' %}"></script>
<script src="{% static 'blog/js/bootstrap.min.js' %}"></script>
</body>
</html>

说明:

  1. JavaScript脚本中的CKEDITOR.replace()函数有两个参数。第一个参数是textarea标签的属性name的值,这个参数设置富文本编辑器放在这个textarea标签中。第二个参数是字典类型参数,字典的每个键名是CKEditor富文本编辑器的属性名,通过字典设置编辑器的长和宽。
  2. CKEDITOR.replace()函数中通过filebrowserBrowseUrl属性设置了处理了浏览图片功能的URL,这里传入的是CKEditor富文本编辑器处理图片浏览功能的视图函数所对应的URL/ckeditor/browse/。

最后是测试,终端启动程序,浏览器输入http://127.0.0.1:8000/test_ckeditor_front/,单击富文本编辑器工具栏上的图片选择按钮,单击上传。

生成数据库表

在settings中设置:

LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/shanghai'

然后在终端迁移数据库:

python manage.py makemigrations
python manage.py migtrate

第一行是对数据库表生成语句进行检查,第二行才是在数据库建表。

建立超级用户

python manage.py createsuperuser

在管理后台注册数据模型

建立好数据模型并生成数据库表后,要在admin中进行注册才能被管理后台管理:

from django.contrib import admin
# 导入建立的数据模型
from .models import Blog,Tag,Category,loguser
# 定义一个自定义数据显示管理模型类,要继承ModelAdmin类
class BlogAdmin(admin.ModelAdmin):
    # 定义了管理后台列表页面上显示的字段
    list_display = ('title','created_time','modified_time','category','author','views')
# 注册loguser,没有自定义管理模型类,将按Django Admin后台默认页面样式进行管理
admin.site.register(loguser)
# 注册博客,有第二个参数,按照BlogAdmin进行管理
admin.site.register(Blog,BlogAdmin)
# 注册Tag,样式进行管理
admin.site.register(Tag)
# 注册Category,默认页面样式进行管理
admin.site.register(Category)

用户注册

URL配置

一级URL:

# 导入后台管理相关模块
from django.contrib import admin
from django.urls import path,include,re_path
# 导入静态文件模块,为了显示上传图片
from django.conf.urls.static import static
from . import settings
urlpatterns = [
    # 自动生成的,封装后台管理URL与视图函数的对应关系
    path('admin/', admin.site.urls),
    # 导入二级URL配置
    path('', include('blog.urls')),
    # 导入二级URL配置
    # path('comments/',include('comments.urls')),
    # 富文本编辑器的URL配置
    path('ckeditor/',include('ckeditor_uploader.urls')),
    path('test_ckeditor_front/',views.test_ckeditor_front),
] + static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT) #没有这一行将无法显示上传的图片

二级URL配置:

from django.urls import path,re_path
from . import views
# 指定当前URL配置为博客应用程序的配置
app_name = 'blog'
urlpatterns = [
    """
    博客首页的URL配置,indexviews继承于通用视图类,不是函数,要用as_view()把类当成函数用
    """
    path('',views.indexview.as_view(),name='index'),
    # 建立URL与注册视图函数register的对应关系
    path('register/',views.register,name='register'),
    path('test_ckeditor_front/',views.test_ckeditor_front),
]

用户注册Form表单

用Django Form建立一个表单,这样可以在代码中控制字段的显示方式、输入校验、错误显示等内容,在blog应用下新建一个forms.py,输入:

# 导入form类
from django import forms
from . import models
# 导入出错信息处理模块
from django.core.exceptions import ValidationError
# 定义一个继承Form类的reg_form类
class reg_form(forms.Form):
    username = forms.CharField(
        max_length=20,
        label='登录账号',
        error_messages={
            'max_length':'登录账号不能超过20位',
            'required':'登录账号不能为空'
        },
        # 页面显示为input标签
        widget=forms.widgets.TextInput(
            attrs={'class':'form-control'},
        )
    )
    password = forms.CharField(
        min_length=6,
        label='密码',
        error_messages={
            'min_length':'密码最少6位',
            'required':'密码不能为空',
        },
        widget=forms.widgets.PasswordInput(
            attrs={'class':'form-control'},
            # 表单数据校验不通过重新返回页面时这个字段输入值还在
            render_value=True
        )
    )
    # 二次输入,保证注册密码正确
    repassword = forms.CharField(
        min_length=6,
        label='确认密码',
        error_messages={
            'min-length':'密码最少6位',
            'required':'密码不能为空',
        },
        widget=forms.widgets.PasswordInput(
            attrs={'class':'form-control'},
            render_value=True,
        )
    )
    nikename = forms.CharField(
        max_length=20,
        required=False,
        label='姓名',
        error_messages={
            'max_length':'姓名长度不能超过20位',
        },
        # 如果不输入nikename字段值,默认为无名氏
        initial='无名氏',
        widget=forms.widgets.TextInput(
            attrs={'class':'form-control'}
        )
    )
    email = forms.EmailField(
        label='邮箱',
        error_messages={
            'invalid':'邮箱格式不对',
            'required':'邮箱不能为空',
        },
        widget=forms.widgets.EmailInput(
            attrs={'class':'form-control'}
        )
    )
    telephone = forms.CharField(
        label='电话号码',
        required=False,
        error_messages={
            'max_length':'最大长度不超过11位',
        },
        widget=forms.widgets.TextInput(
            attrs={'class':'form-control'}
        )
    )
    head_img = forms.ImageField(
        label='头像',
        # 页面生成input,type=file的标签,不显示这个标签
        widget=forms.widgets.FileInput(
            attrs={'style':'display:none'}
        )
    )
    # 定义一个校验字段的函数,校验字段函数命名是有规则的,形式:clean_字段名()
    # 这个函数保证username值不重复
    def clean_username(self):
        # 取得字段值,clean_data保存着通过第一步is_valid()校验的各字段值,是字典类型,
        uname = self.cleaned_data.get('username')
        # 从数据库表中查询是否有同名的记录
        vexist = models.loguser.objects.filter(username=uname)
        if vexist:
            # 如果有同名记录,增加一条错误信息给该字段的errors属性
            self.add_error('username',ValidationError('登录账号已存在'))
        else:
            return uname
    # 定义一个校验程序,判断两次输入的密码是否一致
    def clean_repassword(self):
        passwd = self.cleaned_data.get('password')
        repasswd = self.cleaned_data.get('repassword')
        if repasswd and repasswd != passwd:
            self.add_error('repassword',ValidationError('两次输入的密码不一致'))
        else:
            return repasswd

说明:

代码中定义的字段将在模板文件中生成相应的HTML标签。

代码中定义了校验函数,也可称为钩子函数,它的作用是对表单中的字段进行校验,校验通过逻辑代码进行,钩子函数分局部和全局两种。

  • 局部钩子函数为单个字段设置校验逻辑,命名方式为clean_字段名():

    def clean_字段名(self):
        值变量=self.cleaned_data.get('字段名')
        if 未通过校验:
        	self.add_error('字段名',ValidationError('相关错误信息'))
        else:
            return 值变量
    
  • 全局钩子函数可以对所有字段校验,函数命名为clean()。全局钩子函数不强制要求返回值,也就是可以没有return代码:

    def clean(self):
        ...
        if 未通过校验:
        	self.add_error('字段名',ValidationError('相关错误信息'))
        # 也可以用以下代码
        	raise ValidationError('相关错误信息')
    

django form表单验证可以分为以下4步:

  1. Django Form调用自己原生的校验方法,对每个字段根据max_length、unique等约束进行验证。如果通过则将字段加入clean_data字典集合中。如果不通过则报错或将错误信息放在错误信息列表中,并且不会将对应字段加入clean_data字典。
  2. 调用自定义的clean_字段名()方法(局部钩子函数),对上一步返回的clean_data集合中的该字段名的字段进行校验,不通过的就从clean_data中删去,并将错误放在该字段的错误信息列表中或者抛出错误信息。
  3. 调用自定义的clean()方法(全局钩子函数),对上一步返回的clean_data集合中的所有字段进行校验,不通过的就从clean_data中删去,把错误信息放在字段或表单的错误信息列表中或者抛出错误信息。
  4. 表单错误信息列表中如果有错误,表单is_valid()方法返回false,无错误返回True,说明所有字段通过校验。

用户注册视图函数

表单编写完成需要通过视图函数把表单初始化并传到模板文件,这个视图函数是register(),在views中编写:

from django.shortcuts import render, redirect
from . import models
# 导入form模块,文件中定义了reg_form()
from . import forms
# 导入Django的认证模块,因为代码中要模拟用户登录
from django.contrib.auth.models import auth
# 注册的视图函数
from . import forms
def registe(request):
    if request.method == "POST":
        # form_obj = forms.reg_form(request.POST)#有图片上传,这个句代码是错误的!!!!!!!!!!!
        form_obj = forms.reg_form(request.POST, request.FILES)
        if form_obj.is_valid():  # 判断校验是否通过
            form_obj.cleaned_data.pop("repassword")
            # head_img = request.FILES.get("head_img")
            user_obj = models.loguser.objects.create_user(**form_obj.cleaned_data, is_staff=1, is_superuser=1) # 是系统用户且是超级用户
            # obj.is_staff
            auth.login(request, user_obj)  # 用户登录,可将登录用户赋值给request.user
            return redirect('/')
        else:
            # print(form_obj['repassword'].errors)
            return render(request, "blog/registe.html", {"formobj": form_obj})
    form_obj = forms.reg_form()  # 生成一个form对象
    return render(request, "blog/registe.html", {"formobj": form_obj})

说明:

由于前端页面提交的数据有上传文件数据,存储在request.FILES中,其他存在request.POST中。

调用reg_form类的实例对象form_obj的is_valid()校验字段,通过后每个字段以字典的形式存在cleaned_data中。由于repassword字段只在reg_form类中定义,数据库表中没有这个字段,所以要删去。

数据模型loguser继承自AbstractUser,所以可以用models.loguser.objects.create_user()建立一个系统认证用户。

form_obj.cleaned_data是表单实例对象form_obj的一个属性,是一个字典类型,保存着通过校验的字段的名字和字段值。

auth.login(request, user_obj)这句代码调用认证模块login,相当于user_obj代表的用户对象登录,并将user_obj对象赋值给request.user,这样就可以在代码中用request.user表示user_obj,在模板文件中也可以直接用{{ request.user }}表示user_obj对象。

request.user在任何时间都有值。在无用户登录时request.user是一个AnonymousUser对象,因此不能用if request.user判断是否有用户登录。要判断是否有用户登录可以用if request.user.username语句,因为有用户登录时request.user.username中才能保存username字段的值。

用户注册页面

视图函数registe()通过render()函数将reg_form的实例对象以formobj为名称传给模板文件/templates/blog/registe.html。模板语言根据reg_form中字段的定义。在页面显示出字段内容与样式。

{% load staticfiles %}
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册页面</title>
    <link href="{% static 'blog/css/bootstrap.min.css' %}" rel="stylesheet">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <!--应用Bootstrap页头组件开始 //-->
            <div class="page-header">
                <h2>登录用户注册
                    <small>&nbsp;Blog注册页面</small>
                </h2>
            </div>
            <!--应用Bootstrap页头组件结束 //-->
            <!--form标签设置novalidate,让前端页面表单不对输入的字段值进行验证。设置enctype="multipart/form-data",表单才能支持图片、文件上传。class="form-horizontal"可以将字段名(label标签)和字段输入框水平并排布局。 //-->
            <form novalidate action="/registe/" method="post" class="form-horizontal"   enctype="multipart/form-data">
                {% csrf_token %}
                <div class="form-group">
                    <label for="{{ formobj.username.id_for_label }}"
                           class="col-sm-2 control-label">{{ formobj.username.label }}</label>
                    <div class="col-sm-8">
                        {{ formobj.username }}
                        <span class="help-block">{{ formobj.username.errors.0 }}</span>
                    </div>
                </div>
                <div class="form-group">
                    <label for="{{ formobj.password.id_for_label }}"
                           class="col-sm-2 control-label">{{ formobj.password.label }}</label>
                    <div class="col-sm-8">
                        {{ formobj.password }}
                        <span class="help-block">{{ formobj.password.errors.0 }}</span>
                    </div>
                </div>
                <div class="form-group">
                    <label for="{{ formobj.repassword.id_for_label }}"
                           class="col-sm-2 control-label">{{ formobj.repassword.label }}</label>
                    <div class="col-sm-8">
                        {{ formobj.repassword }}
                        <span class="help-block">{{ formobj.repassword.errors.0 }}</span>
                    </div>
                </div>
                <div class="form-group">
                    <label for="{{ formobj.nikename.id_for_label }}"
                           class="col-sm-2 control-label">{{ formobj.nikename.label }}</label>
                    <div class="col-sm-8">
                        {{ formobj.nikename }}
                        <span class="help-block">{{ formobj.nikename.errors.0 }}</span>
                    </div>
                </div>
                <div class="form-group">
                    <label for="{{ formobj.email.id_for_label }}"
                           class="col-sm-2 control-label">{{ formobj.email.label }}</label>
                    <div class="col-sm-8">
                        {{ formobj.email }}
                        <span class="help-block">{{ formobj.email.errors.0 }}</span>
                    </div>
                </div>
                <div class="form-group">
                    <label for="{{ formobj.telephone.id_for_label }}"
                           class="col-sm-2 control-label">{{ formobj.telephone.label }}</label>
                    <div class="col-sm-8">
                        {{ formobj.telephone }}
                        <span class="help-block">{{ formobj.telephone.errors.0 }}</span>
                    </div>
                </div>
                <div class="form-group">
                    <label class="col-sm-2 control-label">{{ formobj.head_img.label }}</label>
                    <div class="col-sm-8">
                        <label for="{{ formobj.head_img.id_for_label }}"
                               class="col-sm-2 control-label"><img id="head-img" src="/static/blog/image/headimg.jpg"
                                                                   style="height:100px;width:100px;"></label>
                        {{ formobj.head_img }}
                        <span class="help-block"></span>
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-sm-offset-2 col-sm-10">
                        <input type="submit" class="btn btn-success" value="用户注册"></input>
                        <a href="/" class="btn btn-success">返回首页</a>
                    </div>
                </div>
            </form>
        </div>
    </div>
</div>
<script src="{% static 'blog/js/jquery-2.1.3.min.js' %}"></script>
<script src="{% static 'blog/js/bootstrap.min.js' %}"></script>
<script>
    // 找到头像的input标签绑定change事件
    $("#id_head_img").k(function () {
        var filerd = new FileReader(); //  创建一个读取文件的对象
        filerd.readAsDataURL(this.files[0]); // 读取你选中的那个文件
        filerd.onload = function () {
            $("#head-img").attr("src",  filerd.result);// 2. 把图片加载到img标签中
        };
    });
</script>
</body>
</html>

说明:

  1. 我们在/test_blog/blog文件夹下新建一个static文件夹,static文件夹下新建一个blog文件夹。为了分类存放静态文件,再在blog文件夹下新建三个文件夹css、js、image。
  2. 在网页中引用静态文件要让Django知道静态文件的地址,然后在页面文件开头加入{% load static %}。加在静态文件还需配置路径:
    • 在settings中的INSTALLED_APPS中要添加django.contrib.staticfiles,并且设置静态文件URL前缀STATIC_URL = '/static/'。
    • 在应用程序目录创建文件夹static,然后再在这个static文件夹下创建与当前应用名称相同的文件夹,把静态文件放入其中。
    • 完成以上两点就可以引用静态文件了,比如{% static 'blog/js/jquery-2.1.3.min.js' %}中的static会被解析为/项目根目录/应用程序目录/static/。
    • 如果有一些静态文件不放在应用程序目录下,而放在其他目录下,例如放在项目根目录下,那么可以在settings中添加STATICFILES_DIRS,Django会在其下的列表路径中查找静态文件。
    • 页面中的head_img字段是ImageField类型,需要上传图片,我们想实现的功能是单击图片弹出打开窗口。实现思路是在label标签中内嵌img标签,label标签的for属性值为input type="file"标签的id值,这个input标签在reg_form定义中设置为display:none不可见。
    • 选择图片文件上传后,在提交数据前是看不到图片的,也就是不能实现图片预览。为了能预览我们加入JS脚本。在头像的input标签的change事件加入代码,首先建立一个文件读取对象,读入选中的图片文件。然后把这个文件URL赋值给img标签的src属性。onload实现文件完全读到内存后再给img的src。

用户登录

URL配置

在blog/urls中加入

urlpatterns = [
    ...
    # 建立URL与注册视图函数register的对应关系
    path('register/', views.register, name='register'),
    path('login/',views.login,name='login'),
    # 注销的URL配置项
    path('logout/',views.logout,name='logout'),
]

代码中给配置项进行了命名,这样就可以在代码或模板文件中以名字进行反向解析。

用户登录视图函数

login():

def login(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        pwd = request.POST.get('password')
        # 利用auth模块做用户名和密码的校验,也就是用户认证过程
        # 如果不通过,user就是None
        user = auth.authenticate(username=username,password=pwd)
        # 校验通过user才有值,说明user对象是系统认证用户
        if user:
            # 用代码设置用户为登录状态,并将登录用户对象赋值给request.user
            # 也就是说user对象存储在request.user中
            # 可以在代码和模板文件中直接调用request.user
            auth.login(request,user)
            # 登录完成后重定向到博客首页
            return redirect('/')
        else:
            errormsg = "用户名或密码错误"
            return render(request,'blog/login.html',{'error':errormsg})
    # 请求不是POST时打开页面。
    return render(request,'blog/login.html')

说明:

只有Django Admin系统用户才能调用认证相关的函数,如auth.authenticate()、auth,login()。项目中数据模型loguser继承于AbstractUser,AbstractUser中存储的记录都是认证用户对象,让Django用loguser中的用户对象作为认证用户,还需在settings中设置AUTH_USER_MODEL='blog.loguser'。

auth.authenticate(username=username,password=pwd)函数验证用户名和密码。成功就返回一个用户对象,这个用户对象取自AUTH_USER_MODEL设定的数据模型;失败得到None。

auth.login(request,user)函数执行后,会将验证过的用户对象赋值给request.user属性,并且在session表增加一条记录。session_key字段的值是sessionid,session_data字段的值是用户信息,这个值是加密的,同时给浏览器生成一个cookie以记录sessionid。

用户登录页面

templates下blog下的login.html:

{% load static %}
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <meta name="description" content="">
    <meta name="author" content="">
    <title>登录页面</title>
    <link href="{% static 'blog/css/bootstrap.min.css'%}" rel="stylesheet">
</head>

<body>
<div class="container">
    <div class="row">
        <div align="center" style="margin-top:80px"><h2 class="form-signin-heading">请登录</h2></div>
		<!--利用模板语言反向解析URL-->
        <form method="post" action="{% url 'blog:login' %}" class="form-horizontal col-md-6 col-md-offset-3 login-form" >
            {% csrf_token %}

            <div class="form-group">
                <label for="username" class="col-sm-2 control-label">用户名</label>
                <div class="col-sm-10">
                    <input type="text" class="form-control" id="username" name="username" placeholder="用户名">
                </div>
            </div>
            <div class="form-group">
                <label for="password" class="col-sm-2 control-label">密码</label>
                <div class="col-sm-10">
                    <input type="password" class="form-control" id="password" name="password" placeholder="密码">
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-offset-2 col-sm-10">
                    <button class="btn btn-lg btn-primary btn-block" type="submit">登录</button>
                    <span style="color:red">{{ error }}</span>
                </div>
            </div>
        </form>
    </div>
</div> <!-- /container -->
<script src="{% static 'blog/js/jquery-2.1.3.min.js' %}"></script>
<script src="{% static 'blog/js/bootstrap.min.js'%}"></script>
</body>
</html>

博客系统的母版

许多网站为了保持一致,会编写一个母版,网站的页面都继承母版样式。博客系统也采用这种方式,先建一个母版文件,把页面的布局、样式文件放在其中,其他页面都继承于这个母版。

母版HTML文件

在templates文件夹下新建一个base.html:

{% load staticfiles %}
<!-- 导入自定义模板标签 -->
{% load custom_tags %}

<html>
<head>
    <title>blog样例 </title>
    <!-- meta -->
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- css -->
    <link href="{% static 'blog/css/bootstrap.min.css' %}" rel="stylesheet">
    <link rel="stylesheet" href="{% static 'blog/css/blogstyle.css' %}">
    <!-- js -->
    <script src="{% static 'blog/js/jquery-2.1.3.min.js' %}"></script>
    <script src="{% static 'blog/js/bootstrap.min.js' %}"></script>
   <style>
        span.highlighted {
            color: red;
        }
    </style>
</head>

<body>
<!--应用Bootstrap框架的导航条组件-->
<!--导航条组件开始-->
<nav class="navbar navbar-default navbar-fixed-top">
    <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
                    data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="{% url 'blog:index' %}" style="color:red;font-weight:700;">&nbsp;&nbsp;&nbsp;&nbsp;Blog系统简例&nbsp;&nbsp;&nbsp;&nbsp; </a>
        </div>
        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse navbar-left" id="bs-example-navbar-collapse-1">
            <ul class="nav navbar-nav">
                <!-- 这个判断语句中,tabname是视图函数传入的变量,主要标识用户单击了“首页”“我的”中哪一个链接-->
                <li {% if tabname == "firsttab" %} class='active' {% endif %}>
                <a href="{% url 'blog:index' %}" style="color:red;">首页</a></li>
                <!-- 以下模板标签的判断标签判断用户是否已登录,以决定“我的”链接是否显示,注意不能用{% if request.user %},因为无用户登录时,request.user的值为AnonymousUser,但request.user.username无值(None),只有用户登陆了才有值-->
                {% if request.user.username %}
                <li {% if tabname == "mytab" %} class='active' {% endif %}>
                <a href="{% url 'blog:myindex' request.user.id %}" data-hover="我的">我的</a></li>
                {% endif %}
                <!-- 通过一个表单设置搜索字段,注意请求方式是get-->
                <form class="navbar-form navbar-left" method="get" action="{% url 'haystack_search' %}">
                    <div class="form-group">
                        <input type="text" class="form-control" name="q" placeholder="搜索" required>
                    </div>
                    <button type="submit" class="btn btn-default">搜索</button>
                </form>
        </div><!-- /.navbar-collapse -->
        <ul class="nav navbar-nav navbar-right">
            <!--判断用户是否已登录,已决定显示个人中心还是显示登录和注册 -->
            {% if request.user.username %}
            <li><a href="#">{{ request.user.nikename }}</a></li>
            <!-- 应用下拉列表组件 -->
            <li class="dropdown">
                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
                   aria-expanded="false"> 个人中心<span class="caret"></span></a>
                <ul class="dropdown-menu">
                    <li><a href="{% url 'blog:myindex' request.user.id %}">我的文章</a></li>
                    <li role="separator" class="divider"></li>
                    <li><a href="{% url 'blog:logout' %}">注销</a></li>
                </ul>
            </li>
            {% else %}
            <li><a href="{% url 'blog:login' %}">登录</a></li>
            <li><a href="{% url 'blog:registe' %}">注册</a></li>
            {% endif %}
        </ul>
    </div><!-- /.container-fluid -->
</nav>
<!--导航条组件结束-->

<div class="content-body" style="both:clear;margin-top:60px;">
    <div class="container">
        <div class="row">
            <main class="col-md-8">
                <!--模板文件的块,继承母版的页面代码块-->
                {% block main %}
                {% endblock main %}
            </main>
            <aside class="col-md-4">
                <!--模板文件的块toc,这个toc中有代码,那么继承于母版的页面如果在此块中写代码,就替换母版的内容,不写就用母版的内容 -->
                {% block toc %}
                <!--母版页面右边部分有最新文章、分类、标签、归档4个部分,每个栏目都应用面板组件 -->
                <div class="panel panel-primary">
                    <div class="panel-heading">最新文章</div>
                    <div class="panel-body">
                        <!--调用自定义标签文件custom_tags中定义的get_new_blogs()函数,返回最新发表的文章。该函数的工作方式:输入{% get_new_blogs as new_blog_list %},模板就得到一个最新文章列表,并通过as语句保存到new_blog_list模板变量里,后面的语句就可以循环获取每篇文章。-->
                        {% get_new_blogs as new_blog_list %}
                        <ul>
                            {% for blog in new_blog_list %}
                            <li>
                                <a href="{{ blog.get_absolute_url }}">{{ blog.title }}</a>
                            </li>
                            {% empty %}
                            暂无文章!
                            {% endfor %}
                        </ul>
                    </div>
                </div>

                <div class="panel panel-success">
                    <div class="panel-heading">分类</div>
                    <div class="panel-body">
                         <!--调用自定义标签文件custom_tags中定义的get_category()函数,显示每个类中的文章篇数-->
                        {% get_categories as category_list %}
                        <ul>
                            {% for category in category_list %}
                            <li>
                                <a href="{% url 'blog:category' category.pk %}">{{ category.name }} <span
                                        class="post-count">({{ category.num_blogs}})</span></a>
                            </li>
                            {% empty %}
                            暂无分类!
                            {% endfor %}
                        </ul>
                    </div>
                </div>

                <div class="panel panel-info">
                    <div class="panel-heading">标签</div>
                    <div class="panel-body">
                        <div class="tag-list">
                            <!--调用自定义标签文件custom_tags中定义的get_tags()函数,显示每个标签的名字和文件数量-->
                            {% get_tags as tag_list %}
                            <ul >
                                {% for tag in tag_list %}
                                <li><a href="{% url 'blog:tag' tag.pk %}">{{ tag.name }}({{ tag.num_blogs }})</a>
                                </li>
                                {% empty %}
                                暂无标签!
                                {% endfor %}
                            </ul>
                        </div>
                    </div>
                </div>
                <div class="panel panel-default">
                    <div class="panel-heading">归档</div>
                    <div class="panel-body">
                        <!--调用自定义标签文件custom_tags中定义的archives()函数,倒序显示有文章发表的年月-->
                        {% archives as date_list %}
                        <ul>
                            {% for date in date_list %}
                            <li>
                                <a href="{% url 'blog:archives' date.year date.month %}">
                                    {{ date.year }} 年 {{ date.month }}月</a>
                            </li>
                            {% empty %}
                            暂无归档!
                            {% endfor %}
                        </ul>
                    </div>
                </div>

                {% endblock toc %}
            </aside>
        </div>
    </div>
</div>
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <div align="center">
                <h5>&copy 2017 - good good study day day up - 坚持每天进步一点</h5>
            </div>

        </div>
    </div>
</div>
</body>
</html>

说明:

  1. 母版样式采用Bootstrap框架,下图是导航条组件:

    image-20210718163041264

  2. 母版通过调用自定义模板标签,给页面的最新文章、分类、标签、归档4个栏目传递数据,这些提供数据的自定义标签都放在blog_tags文件中,因此开头要引入。

  3. 引入了一个css文件对样式微调,在/blog/static/blog/css/blogstyle.ss:

    /*链接样式*/
    a:hover, a:focus {
        text-decoration: none;
        color: #000;
    }
    
    /*文章标题*/
    .entry-title {
        text-align: center;
        font-size: 1.9em;
        margin-bottom: 10px;
        line-height: 1.6;
        padding: 10px 20px 0;
    }
    /*文章内容*/
    .blog {
        background: #fff;
        padding: 30px 30px 0;
    }
    /*文章属性字段*/
    .entry-meta {
        text-align:left;
        color: #DDDDDD;
        font-size: 13px;
        margin-bottom: 30px;
      }
    
    /*文章分类属性字段*/
    .entry-meta-detail {
        text-align:center;
        color: #DDDDDD;
        font-size: 13px;
        margin-bottom: 30px;
      }
    
    
    .blog-category::after,
    .blog-date::after,
    .blog-author::after,
    .comments-link::after {
        content: ' ·';
        color: #000;
    }
    /*文章内容*/
    .entry-content {
        font-size: 16px;
        line-height: 1.9;
        font-weight: 300;
        color: #000;
    }
    /*评论格式*/
    .comment-area {
        padding: 0 10px 0;
    }
    /*标签Tag的样式*/
    
    .tag-list ul {
        padding: 0;
        margin: 0;
        margin-right: -10px;
    }
    .tag-list ul li {
        list-style-type: none;
        font-size: 13px;
        display: inline-block;
        margin-right: 10px;
        margin-bottom: 10px;
        padding: 3px 8px;
        border: 1px solid #ddd;
    }
    

项目的自定义标签

在这个博客系统中我们用自定义标签为母版文件的4个栏目提供数据,这个编写自定义标签的文件需要存放在固定文件夹中。首先在/test_blog/blog/下创建一个templatetags文件夹,然后在这个文件夹创建一个custom_tags.py文件,用这个文件存放自定义的模板标签代码:

# 实例化了一个template.Library类,是固定写法
register = template.Library()
# 将函数get_new_blogs()装饰为register.simple_tag
# 这样就可以在模板文件中使用{% get_new_blogs %}调用这个函数
@register.simple_tag
def get_new_blogs(num=5):
    # 通过Django ORM查询语句返回最新的5篇文章
    # 通过按created_time字段倒序和切片操作实现
    return Blog.objects.all().order_by('-created_time')[:num]
@register.simple_tag
def archives():
    # dates()函数返回一个列表,列表中的文章的创建时间精确到月份,降序排列
    return Blog.objects.dates('created_time','month',order='DESC')
@register.simple_tag
def get_categories():
    # 通过Django分类聚合函数统计每个分类中的文章的数量,并过滤没有文章的分类
    return Category.objects.annotate(num_blogs=Count('blog')).filter(num_blogs__gt=0)
@register.simple_tag
def get_tags():
    # 通过Django分类聚合函数统计每个标签中的文章的数量,并过滤没有文章的标签
    return Tag.objects.annotate(num_blogs=Count('blog')).filter(num_blogs__gt=0)

说明:

编写自定义模板标签需要注意三点:一是导入template这个模块,二是通过register=template.Library()语句实例化了一个template.Library类,三是用@register.simple_tag语句装饰函数。

母版中的4个栏目链接功能实现

最新文章栏目链接功能实现

在base.html文件中,最新文章栏目中为每篇博客文章生成的链接如下所示,这个链接中blog是Blog的实例对象,它调用Blog数据模型类的get_absolute_url()方法,这个方法返回一个URL。

<a href="{{ blog.get_absolute_url }}"></a>

Blog类的get_absolute_url方法如下

def get_absolute_url(self):
    return reverse('blog:detail',kwargs={'pk':self.pk})

在blog/urls中的一个配置项名字是detail,而Blog类的get_absolute_url()方法中反向解析的名字也是detail。

re_path('blog/(?P<pk>[0-9]+)',views.blogdetailview.as_view(),name='detail'),

由以上3句代码可以推测母版上链接的URL对应的视图是blogdetailviews,因为这个视图是一个类,所以通过as_view()转为函数。

在blog/views中可以看到这个视图:

from django.views.generic import DetailView
# 视图继承于DetailView通用视图类
class blogdetailview(DetailView):
    # 指定数据模型,从中取出一条记录
    model = models.Blog
    # 指定模板文件
    template_name = 'blog/detail.html'
    # 指定传给模板文件的变量名
    context_object_name = 'blog'
    # pk_url_kwarg指定取得一条记录的主键值。pk是指配置项中的URL表达式中的参数名
    # 可以理解为获取主键值等于URL表达式中参数pk值的数据记录。
    pk_url_kwarg = 'pk'
    # 重写父类get_object()方法,常用于返回定制的数据记录。
    def get_object(self, queryset=None):
        blog=super(blogdetailview,self).get_object(queryset=None)
        blog.increase_views()
        return blog
    # 重写get_context_data()方法,常用于增加数据模板变量
    def get_context_data(self, **kwargs):

分类栏目链接功能实现

在母版base.html中,在分类栏目中为每个类别生成的链接如下,通过{% url %}模板标签利用URL配置项名字和参数解析出URL。

<a href="{% url 'blog:category' category.pk %}"></a>

在url中相关配置如下,命名为category。

re_path('category/(?P<pk>[0-9]+)/',views.categoryview.as_view(),name='category'),

视图函数categoryview:

from django.views.generic import ListView
class categoryview(ListView):
    model = models.Blog
    template_name = 'blog/index.html'
    context_object_name = 'blog_list'
    def get_queryset(self):
        cate=get_object_or_404(models.Category,pk=self.kwargs.get('pk'))
        return super(categoryview,self).get_queryset().filter(category=cate).order_by('-created_time')

说明:

  1. categoryview继承于通用视图类ListView,可以通过属性model、template_name、context_object_name分别设置数据模型、模板文件、模板变量名。需要注意model与context_object_name的关系,也就是从model指定的数据模型取出记录,保存在以context_object_name为名字的变量中传递给模板文件。
  2. URL配置项的URL表达式('category/(?P<pk>[0-9]+/)')中的参数pk值在视图类中可以用self.kwargs.get('pk')取得。
  3. get_object_or_404函数的作用是调用Django的get方法查询获取的数据。如果查询的对象不存在的话,则返回一个404错误页面。
  4. 视图类ListView有个方法get_queryset(),这方法默认取得数据模型的全部数据。如果想要取得定制就要重写这个方法。如视图categoryview中的代码,首先取得Category中的一个对象(一个分类类别),然后调用父类get_queryset(),对返回的queryset集合(默认是models.Blog中的全部记录)进行过滤,取得这个分类类别的全部记录(该分类下的全部文章),同时对全部记录进行了排序。这些记录会以blog_list为变量名传给模板文件。

标签栏目链接功能实现

在母版base.html中,标签栏目中为每个标签生成的链接如下所示

<a href="{% url 'blog:tag' tag.pk %}"></a>

在url中相关的匹配项如下,命名为tag。

re_path('tag/(?P<pk>[0-9]+)/',views.tagview.as_view(),name='tag'),

tagview视图函数:

class tagview(ListView):
    model = models.Blog
    template_name = 'blog/index.html'
    context_object_name = 'blog_list'
    def get_queryset(self):
        tag=get_object_or_404(models.Tag,pk=self.kwargs.get('pk'))
        return super(tagview,self).get_queryset().filter(tags=tag).order_by('created_time')

归档栏目链接功能实现

在母版base.html中,在归档栏目中为每个归档生成的链接如下,这里传递两个参数:date.year、date.month。

<a href="{% url 'blog:archives' date.year date.month %}"></a>

在url中相关的匹配项如下,有year和month两个url参数。该项被命名为archives:

re_path('archives/(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/',views.archives,name='archives')

archives视图函数:

def archives(request,year,month):
    blog_list = month.Blog.objects.filter(created_time__year=year,created_time__month=month).order_by('-created_time')
    return render(request,'blog/index.html',context={'blog_list':blog_list})

说明:

  1. archives中增加两个参数year和month,对应的URL表达式有两个命名参数year和detail。Django会从用户访问的URL中自动提取这两个参数的值,然后传递给其对应的视图函数。
  2. 代码中使用filter函数按条件过滤数据库表记录,由于传入参数是year和month,需要用created_time字段的year和month属性过滤。根据Django的规则,created_time__year可以取得创建时间的年的部分。

母版其他功能

“我的”功能实现

在母板文件中有个链接是为了显示自己发布的文章,这个链接只有在用户登录后才显示:

<a href="{% url 'blog:myindex' request.user.id %}" data-hover="我的">我的</a>

在urls中有一个匹配项名字是myindex

path('myindex/<int:loguserid>/',views.myindex.as_view(),name='myindex'),

views中的myindex类:

class myindex(ListView):
    model = models.Blog
    template_name = 'blog/index.html'
    context_object_name = 'blog_list'
    def get_queryset(self):
        loguser=get_object_or_404(models.loguser,pk=self.kwargs.get('loguserid'))
        return super(myindex,self).get_queryset().filter(author=loguser).order_by('-created_time')
    def get_context_data(self, *, object_list=None, **kwargs):
        # 调用父类生成保存模板变量的字典
        context = super(myindex,self).get_context_data(**kwargs)
        context['tabname']='mytab'
        return context

“注册”“登录”功能实现

打开博客系统的首页时,如果没有登录(request.user.username=='none'),在导航条会显示“注册”“登录”两个链接,连接的地址通过URL反向解析得到,相关代码在base.html中。

<li><a href="{% url 'blog:login' %}">登录</a></li>
<li><a href="{% url 'blog:register' %}">注册</a></li>

个人中心功能实现

如果用户已经登录,在博客系统首页的导航条会显示个人中心链接,相关代码也在base.html。

<li class="dropdown">
                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
                   aria-expanded="false"> 个人中心<span class="caret"></span></a>
                <ul class="dropdown-menu">
                    <li><a href="{% url 'blog:myindex' request.user.id %}">我的文章</a></li>
                    <li role="separator" class="divider"></li>
                    <li><a href="{% url 'blog:logout' %}">注销</a></li>
                </ul>
            </li>

说明:

以上代码引用了Bootstrap中的下拉列表框组价。

代码中的我的文章链接调用通用视图类myindex。

注销链接调用视图函数logout():

def logout(request):
    # 调用认证模块,执行logout函数,这样会把用户相关的cookie、session清空
    auth.logout(request)
    return redirect()
posted @ 2021-07-19 23:31  KKKyrie  阅读(425)  评论(0编辑  收藏  举报