博客系统开发-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='文档管理表'
说明:
- 字段category存储博客文章类别,其值是ForeignKey类型,这样指定一条博客文章记录只能属于一个分类,一条类别记录可以有多条博客文章记录相对应。数据模型类中有ForeignKey属性的字段在数据库表中的字段名为“属性名_id”,category在数据库表中的字段名是category_id。该字段与数据库表Category中的主键id字段是多对一的关联关系。
- 字段tags存储博客文章标签,是多对多键,一条博客文章记录可以对应多条标签记录,一条标签记录可以对应多条博客文章记录。
- 在一个数据模型中新增一个多对多键,在生成数据库表时,并没有在数据库表中生成这个字段,而是额外生成一张数据库表把有多对多关系的两个表关联起来,表的命名格式为“应用程序名_数据模型名_多对多键字段名”,表名都是小写,本项目中表名为blog_blog_tags。这个表有3个字段。分别为主键id和两个外键,两个外键关联两个多对多关系的数据库表,两个外键字段命名格式为“数据模型名_id”。本项目中字段名为blog_id和tag_id。
- 字段author存储博客文章作者的名字,这个字段是外键类型,与loguser数据库表形成多对一关系,指明了一篇文章只能有一个作者,一个作者可写多篇文章。
- 数据模型中get_absolute_url()方法主要作用是返回一个url,调用redirect(obj)函数时,如果这个obj是这个数据模型实例对象(相当于一条表记录),redirect执行后重定向到该obj对象的get_absolute_url()方法返回的URL。
- 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/。
- 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='用户信息表'
说明:
- Django内置的User数据模型包含username、password、email、first_name、last_name等属性,这里我们建立loguser数据模型类,增加了nikename、telephone、head_img属性。
- 数据模型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})
说明:
- 根据django-ckeditor用法,为了实现前端页面上编辑器有图片上传权限,我们利用Django认证模块auth中的login()函数模拟一个系统用户登录。
- 数据模型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>
说明:
- JavaScript脚本中的CKEDITOR.replace()函数有两个参数。第一个参数是textarea标签的属性name的值,这个参数设置富文本编辑器放在这个textarea标签中。第二个参数是字典类型参数,字典的每个键名是CKEditor富文本编辑器的属性名,通过字典设置编辑器的长和宽。
- 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步:
- Django Form调用自己原生的校验方法,对每个字段根据max_length、unique等约束进行验证。如果通过则将字段加入clean_data字典集合中。如果不通过则报错或将错误信息放在错误信息列表中,并且不会将对应字段加入clean_data字典。
- 调用自定义的clean_字段名()方法(局部钩子函数),对上一步返回的clean_data集合中的该字段名的字段进行校验,不通过的就从clean_data中删去,并将错误放在该字段的错误信息列表中或者抛出错误信息。
- 调用自定义的clean()方法(全局钩子函数),对上一步返回的clean_data集合中的所有字段进行校验,不通过的就从clean_data中删去,把错误信息放在字段或表单的错误信息列表中或者抛出错误信息。
- 表单错误信息列表中如果有错误,表单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> 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>
说明:
- 我们在/test_blog/blog文件夹下新建一个static文件夹,static文件夹下新建一个blog文件夹。为了分类存放静态文件,再在blog文件夹下新建三个文件夹css、js、image。
- 在网页中引用静态文件要让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;"> Blog系统简例 </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>© 2017 - good good study day day up - 坚持每天进步一点</h5>
</div>
</div>
</div>
</div>
</body>
</html>
说明:
-
母版样式采用Bootstrap框架,下图是导航条组件:
-
母版通过调用自定义模板标签,给页面的最新文章、分类、标签、归档4个栏目传递数据,这些提供数据的自定义标签都放在blog_tags文件中,因此开头要引入。
-
引入了一个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')
说明:
- categoryview继承于通用视图类ListView,可以通过属性model、template_name、context_object_name分别设置数据模型、模板文件、模板变量名。需要注意model与context_object_name的关系,也就是从model指定的数据模型取出记录,保存在以context_object_name为名字的变量中传递给模板文件。
- URL配置项的URL表达式('category/(?P<pk>[0-9]+/)')中的参数pk值在视图类中可以用self.kwargs.get('pk')取得。
- get_object_or_404函数的作用是调用Django的get方法查询获取的数据。如果查询的对象不存在的话,则返回一个404错误页面。
- 视图类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})
说明:
- archives中增加两个参数year和month,对应的URL表达式有两个命名参数year和detail。Django会从用户访问的URL中自动提取这两个参数的值,然后传递给其对应的视图函数。
- 代码中使用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()