项目流程:
1. 产品需求
一. 基于用户认证组件和Ajax实现登陆(图片验证码)
二. 基于forms组件和Ajax实现用户注册功能
一和二的数据库表可以放入到用户信息表中
三.系统首页设计
四.设计个人站点
四的个人网站设计了字段为 title(个人网站的名字),site_name(个人网站URL后面的字段,比如 http:xxx/alex),theme(个人网站主题的CSS样式), 然后和用户信息表做一对一关联
五.个人文章详情页
六.文章点赞功能
七.实现评论功能
--------文章的评论
--------对评论的评论
八. 后台管理页面(富文本编辑框)
2. 设计表结构
2.1 用户信息表
使用session功能,利用django自带的auth功能,但是自带的auth表字段很少
原生的auth_user表中只有id,password等一些基础的字段.无法满足具体业务的认证需求.
所以我们可以自定义一张表,来写入需要用到的字段,因为原生的User表是继承了AbstractUser 类.
所以我们也可以继承并扩展这个表
使用的时候要注意setting.py中要添加 AUTH_USER_MODEL = "app02.UserInfo"
app02是应用名,UserInfo是自定义类名.
否则容易出现报错
from django.contrib.auth.models import User,AbstractUser
'''
这里AbstractUser 父类就已经有了username password等一些列的字段了,
其他的只是我们扩展的字段而已
'''
class UserInfo(AbstractUser):
nid = models.AutoField(primary_key=True)
telphone = models.CharField(max_length=11, null=True, unique=True)
avatar = models.FileField(upload_to="avatars/", default="avatars/default.png")
create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
blog = models.OneToOneField(to="Blog", to_field="nid", null=True,on_delete=models.CASCADE)
def __str__(self):
return self.username
2.2 个人主页表
个人主页表放入了主页的标题,站点名,和网站样式
class Blog(models.Model):
nid = models.AutoField(primary_key=True)
title = models.CharField(verbose_name="个人博客标题", max_length=64)
sitename = models.CharField(verbose_name="站点名称", max_length=64)
theme = models.CharField(verbose_name="个人博客主题央视", max_length=32)
def __str__(self):
return self.title
2.3 分类
用来定义文章的内容类别 ,比如python,java,mysql, 和个人主页文章表属于一对一关系,也就是一篇文章有一个分类
同时分类和用户信息表应该是属于一对多关系,一个用户下可以有多个分类
注意点:因为用户信息表和个人主页表是一对一关系,所以我们的分类表和他们任意一张表建立一对多关系,其他表都是可以通过跨表查询到的
class Category(models.Model):
'''个人文章分类表'''
nid = models.AutoField(primary_key=True)
title = models.CharField(verbose_name="分类标题", max_length=64)
blog = models.ForeignKey(to="Blog", verbose_name="所属博客", to_field="nid",on_delete=models.CASCADE)
def __str__(self):
return self.title
2.4 标签
用来定义文章的关键字信息 和个人主页文章表属于一对多关系,也就是一篇文章有多个关键字,同时标签和用户信息表应该是属于一对多关系,一个用户下可以有多个标签
注意点:因为用户信息表和个人主页表是一对一关系,所以我们的标签表和他们任意一张表建立一对多关系,其他表都是可以通过跨表查询到的
class Tag(models.Model):
'''个人文章标签表'''
nid = models.AutoField(primary_key=True)
title = models.CharField(verbose_name="标签名称", max_length=64)
blog = models.ForeignKey(to="Blog", verbose_name="所属博客", to_field="nid",on_delete=models.CASCADE)
def __str__(self):
return self.title
2.5 个人主页文章表
class Artical(models.Model):
'''个人文章分类表'''
nid = models.AutoField(primary_key=True)
title = models.CharField(verbose_name="文章标题", max_length=64)
desc = models.CharField(verbose_name="正文描述",max_length=32)
create_time = models.DateTimeField(verbose_name="文章创建时间", auto_now_add=True)
content = models.TextField() # 文章正文
comment_count = models.IntegerField(verbose_name="文章评论数", default=0)
up_count = models.IntegerField(verbose_name="点赞数", default=0)
down_count = models.IntegerField(verbose_name="踩他数", default=0)
# 文章和用户 一对多关系,字段放在多的这张表
user = models.ForeignKey(to="UserInfo", to_field="nid", verbose_name="作者", on_delete=models.CASCADE)
# 文章和分类,可以一对多,也可以多对多,这里设置成一对多
category = models.ForeignKey(to="Category", to_field="nid", verbose_name="文章分类", on_delete=models.CASCADE)
# 文章和标签,设置成多对多关系,不用through,就是系统自动创建关联表,用through就是自动创建第三张关联表
tag = models.ManyToManyField(
to="Tag",
through="Artical2Tag",
through_fields=("article", "tag")
)
def __str__(self):
return self.title
文章和标签tag多对多的第三张自定义关系表
class Artical2Tag(models.Model):
nid = models.AutoField(primary_key=True)
article = models.ForeignKey(to="Artical", to_field="nid", on_delete=models.CASCADE, verbose_name="文章")
tag = models.ForeignKey(to="Tag", to_field="nid", on_delete=models.CASCADE, verbose_name="标签")
#通过一个内嵌类 "class Meta" 给你的 model 定义元数据
#这里unique_together 代表 article和tag 他们两者的且关系必须是唯一的
class Meta:
unique_together = [
("article", "tag")
]
def __str__(self):
ret = self.article.title + "---" +self.tag.title
return ret
2.6 点赞和踩他
class ArticleUpDown(models.Model):
#文章点赞 踩他表
nid = models.AutoField(primary_key=True)
article = models.ForeignKey(to="Artical", to_field="nid", on_delete=models.CASCADE, verbose_name="文章",null=True)
#之所以需要user是因为对文章的点赞 一定是对某个作者的某篇文章进行点赞,而不是单独对某个文章点赞
user = models.ForeignKey(to="UserInfo", to_field="nid", verbose_name="作者", on_delete=models.CASCADE,null=True)
is_up = models.BooleanField(default=True)
class Meta:
unique_together = [
("article", "user")
]
2.7评论
class Comment(models.Model):
# 文章点赞 踩他表
nid = models.AutoField(primary_key=True)
article = models.ForeignKey(to="Artical", to_field="nid", on_delete=models.CASCADE, verbose_name="文章", null=True)
user = models.ForeignKey(to="UserInfo", to_field="nid", verbose_name="作者", on_delete=models.CASCADE,null=True)
creat_time = models.DateTimeField(verbose_name="评论创建日期",auto_now_add=True)
content=models.CharField(verbose_name="评论内容",max_length=255)
# 这个字段主要用于对评论的评论,to=self或者Comment 是一种自关联的写法
parent_comment = models.ForeignKey(to="self",null=True,on_delete=models.CASCADE)
'''
对评论的评论逻辑如下: 444和555是对111评论的评论
111
444
555
222
333
Comment 表
parent_comment 为null 表示只是对文章的评论,
而最后两条分别是对nid为1的评论和nid为4的评论的评论
nid article user create_time content parent_comment
1 1 1 2021-09-08 111 null
2 2 1 2021-09-09 222 null
3 1 1 2021-09-09 333 null
4 1 1 2021-09-10 444 1
5 1 1 2021-09-11 555 4
'''
3.对着每个功能进行开发
功能1 登陆页面
-
基于form组件渲染登陆页面和注册页面的标签元素
form组件字段用到的有
required=False 是否必填
,error_messages={"required": "账号不能为空", "invalid": "账号格式有误"} 自定义错误信息提示
widget=widgets.PasswordInput(attrs={"class": "form-control"}, )) 内置的widgt插件,设置标签属性等
-
注册和登陆页面的验证码实现
简单的文字验证码,用PIL模块,
先创建实例Image.new(),创建一块画布,
然后设置ImageFont.truetype字体样式
再创建画笔实例ImageDraw.Draw(),通过随机生成的大小写和数字进行填写,注意每个字符的间距
随机的字符串生成后,通过request.session,同步到session会话中,用于后期的验证码校验
创建BytesIO()实例,把生成的验证码写入进去,这样就不用在硬盘上生成图片调用了,加快代码的执行效率.
验证图片的刷新可以通过按钮刷新,也可以在图片的URL后添加?实现重复刷新
$("#verify_code").click(function () { $(this)[0].src += "?"; });
-
ajax功能实现用户账号密码POST校验.
不要忘记添加CSRF字段一起POST,ajax的字段提交常用到的就是url,type,data,和成功回调函数success
-
后台session进行账号密码校验
先通过Django自带的auth模块进行校验(auth.authenticate),校验成功后(auth.login)进行注册,返回一个request.user.
校验失败,则返回一个字典,通过ajax里的success进行渲染到前端网页,进行打印提示.
-
用户注册功能,也是用form组件进行字段渲染,同时对于用户上传自定义头像图片实现逻辑如下:
用户头像上传预览的方式1,通过label标签的for和input id一致产生的联动性实现的,点击了label也会出发input的聚焦事件,然后把#user_icon标签进行显示隐藏 html: <div> <label for="user_icon">用户头像 <img class="head_pic" src="/static/img/default.jpg"></img> </label> <input type="file" id="user_icon"> </div>
方式2,通过子绝父相让头像图片(head2_img)和input框(head2_input)重叠,并且大小和长宽设置一样 然后input(head2_input)透明度设置为透明 <div class="user_head2_div" style="height: 60px"> <span>用户头像2</span> <img src="/static/img/default.jpg" class="head2_img"> <input type="file" id="head2_input"> </div> <div> <input type="button" class="btn btn-success" value="确认提交"> </div>
-
用户头像预览
前端考虑到功能是上传图片后实现的图片预览,所以选用的是change事件,然后通过files获取用户选中的文件对象,
然后用js的 FileReader() 类读取文件内容,并返回 class.result()
修改img的src属性,设置src的值为class.result()文件读取结果的值.
用onload事件对图片进行加载显示.
$("#user_icon").change(function () {
//用change事件获取文件对象
var file_obj = $(this)[0].files[0];
//实例化一个FileRead类,用readAsDataURL读取图片内容
var head_icon = new FileReader();
head_icon.readAsDataURL(file_obj);
//用该标签的加载事件进行对图片的前端渲染
head_icon.onload = function () {
$(".head_pic").attr("src", head_icon.result)
};
})
-
ajax中利用js的form表单的进行数据提交
创建new.FormData()实例,然后用append方法添加键值.头像的值获取方法同上
利用FormData提交数据时候,AJAX的contentType和processData需要设置为false
注意csrftoken的键值提交
ajax数据POST方式1:
$.ajax({ type:"POST", //ajax提交form 情况下 contentType和processData 必须为false contentType:false, processData:false, dataType:"json", data:{ "user": $("#id_reg_account").val(), 'csrfmiddlewaretoken':$("input[name='csrfmiddlewaretoken']").val(), //#todo 怎么在ajax中发送csrf而不是在获取html数值后续研究下 "csrfmiddlewaretoken":$.cookie("csrftoken"), }, success:function (data) {} })
ajax数据POST方式2:
我们也可以对data中的键值进行用js中的FormData()类进行统一添加,然后赋值给data
$(".submit").click(function () { var post_data = new FormData(); //js中使用FormData类进行ajax中的data数据提交 //创建new.FormData()实例,然后用append方法添加键值.头像的值获取方法同上 //append的key值要注意和html中的name相互对应 post_data.append("reg_account",$("#id_reg_account").val()); post_data.append("pwd",$("#id_pwd").val()); post_data.append("re_pwd",$("#id_re_pwd").val()); post_data.append("email",$("#id_email").val()); post_data.append("avatar",$("#user_icon")[0].files[0]); //利用FormData提交数据时候,AJAX的contentType和processData需要设置为false post_data.append("csrfmiddlewaretoken",$("input[name='csrfmiddlewaretoken']").val()); //注意csrftoken的键值提交 $.ajax({ type:"POST", //ajax提交form 情况下 contentType和processData 必须为false contentType:false, processData:false, dataType:"json", data:post_data, success:function (data) {} } )
ajax数据POST方式3:
方式2还可以再优化,注意到添加的键值对除了'#avatar'是一个文件对象,其他都是可以通过遍历进行动态获取form表单中的键值对,利用form对象的serializeArray()方法,这个方法会把里面的键值对组成数组
var post_data = new FormData(); //js中使用FormData类进行ajax中的data数据提交 //创建new.FormData()实例,然后用append方法添加键值.头像的值获取方法同上 //append的key值要注意和html中的name相互对应 post_data.append("reg_account",$("#id_reg_account").val()); post_data.append("pwd",$("#id_pwd").val()); post_data.append("re_pwd",$("#id_re_pwd").val()); post_data.append("email",$("#id_email").val()); // 可以获取form表单的对象,用serializeArray()方法,把里面的key和value分别用name和value获取,遍历赋值,图片对象特殊无法用这种方法获取,需要下面手动append var request_data = $("#form").serializeArray(); $(request_data).each(function (index,data) { post_data.append(data.name,data.value) });
-
django的UserForm组件对于前端ajax提交的数据进行清理校验
form.is_valid()>>>form.cleaned_data和form.errors,对form.errors的错误信息进行对应的显示
def reg(request): """注册账号用户""" if request.is_ajax(): #也可以用request.menth=="POST",因为ajax也是一次局部post reg_info = reg_form(request.POST) response={"user":None,"message":None} if reg_info.is_valid(): #如果账号密码通过了局部钩子和全局钩子 response["user"]=reg_info.get("user") else: #未通过钩子校验 response["message"] = reg_info.errors return JsonResponse(response) new_regs = reg_form() return render(request, "reg.html", {"new_regs": new_regs})
-
前端对返回的错误字典进行前端提示
success:function (data) { //先清空span标签中的错误信息和边框颜色 $(".error_info").text(""); $(".form-control").css({"border":"1px solid black"}); if(data.user){ //获取到user说明POST数据校验成功 }else{ //否则单元格显示错误信息 //Django返回的key是{user:None,message:None} $.each(data.message,function (key,error_list) { //通过id_标签名的拼接,定位dom元素,然后在他的span标签中显示错误样式和错误信息 $("#id_"+key).next(".error_info").text(error_list[0]).css({"color":"red","font-weight":"bolder"}); $("#id_"+key).css({"border":"1px solid red"}) // $("#id_"+key).parent().addClass("has-error") }) } }
先清空span标签中的错误提示和边框标红的效果.然后遍历错误的字典,遍历错误的键值,因为我UserForm组件渲染的标签id命名规则是id_+字段,所以我们也同样构造获取对象
$("#id_"+key).next(".error_info")
,并利用next方法获取同级别元素中的class=error_info类名进行错误信息的赋值和边框的颜色变更.ajax中的success:function(data)中的data返回值如下,message是他的键,值是2个错误信息
-
Django对于用户名,密码和二次密码进行局部钩子和全局钩子验证,通过验证后写入数据库
class reg_form(forms.Form): reg_account = forms.CharField( max_length=32, label="注册新账名", error_messages={"required": "账号不能为空", "invalid": "账号格式有误"}, widget=widgets.TextInput(attrs={"class": "form-control"}, )) pwd = forms.CharField(min_length=3,max_length=32, label="输入密码", error_messages={"required": "密码不能为空", "invalid": "密码请按大小加特殊字符"}, widget=widgets.PasswordInput(attrs={"class": "form-control"}, )) re_pwd = forms.CharField(min_length=3,max_length=32, label="确认密码", error_messages={"required": "确认密码不能为空", "invalid": "密码请按大小加特殊字符"}, widget=widgets.PasswordInput(attrs={"class": "form-control"}, )) email = forms.EmailField(max_length=32, required=False, label="邮箱", widget=widgets.EmailInput(attrs={"class": "form-control"}, )) def clean_reg_account(self): """局部钩子,函数命名规则clean_校验字段""" user = self.cleaned_data.get("reg_account") user_check = UserInfo.objects.filter(username = user) if not user_check: return user else: raise ValidationError("用户名已注册") def clean(self): # 全局钩子,对几个字段进行额外的校验 pwd = self.cleaned_data.get("pwd") re_pwd = self.cleaned_data.get("re_pwd") #密码和二次输入的密码不为空但是2次输入不一致 #全局钩子的错误key值是__all__开头的,因此需要在前端额外对这个字段进行过滤提取,显示在网页上 if pwd and re_pwd and not (pwd == re_pwd): raise ValidationError("2次密码输入不一致") # 密码和二次输入的密码不为空且相同 else: # 按照模块的写法,如果没问题返回cleaned_data return self.cleaned_data def reg(request): """注册账号用户""" if request.is_ajax(): #也可以用request.menth=="POST",因为ajax也是一次局部post reg_info = reg_form(request.POST) response={"user":None,"message":None} if reg_info.is_valid(): #通过了全局钩子和局部钩子 response["user"]=reg_info.cleaned_data.get("reg_account") user = reg_info.cleaned_data.get("reg_account") pwd = reg_info.cleaned_data.get("pwd") email = reg_info.cleaned_data.get("email") avatar = request.FILES.get("avatar") # 如果账号密码通过了局部钩子和全局钩子,就把注册信息写入到数据库中 # 要用create_user方法,会进行加密存储 UserInfo.objects.create_user(username=user,password=pwd,email=email,avatar=avatar) else: #未通过钩子校验,把错误字段放入message中 response["message"] = reg_info.errors return JsonResponse(response) new_regs = reg_form() return render(request, "reg.html", {"new_regs": new_regs})
-
全局钩子的错误key值是__all__开头的,因此需要在前端额外对这个字段进行过滤提取,显示在网页上
success:function (data) { console.log(data); //先把错误信息显示的效果清除 $(".error_info").text(""); $(".form-control").css({"border":"1px solid black"}); if(data.user){ //获取到user说明注册成功,跳转到登陆页面 location.href="/cnblog/login/" }else{ // console.log(data); //否则单元格显示错误信息 //先清空span标签中的错误信息和边框颜色 //Django返回的key是{user:None,message:None} $.each(data.message,function (key,error_list) { //提取全局钩子的错误key值,过滤出来显示前端 if(key=="__all__"){ $("#id_re_pwd").next(".error_info").text(error_list[0]).css({"color":"red","font-weight":"bolder"}); }; $("#id_"+key).next(".error_info").text(error_list[0]).css({"color":"red","font-weight":"bolder"}); $("#id_"+key).css({"border":"1px solid red"}) // $("#id_"+key).parent().addClass("has-error") }) } },
-
avatar路径设置
我们在自定义ORM表的时候是这么定义avatar字段的,上传的路径为avatars 文件夹, 在当前项目的根路径下如果有avatars文件夹,则会把文件以`Blogavatars1.jpg`形式存储. 如果没有这个文件夹则会自动创建文件夹 ```python class UserInfo(AbstractUser): nid = models.AutoField(primary_key=True) telphone = models.CharField(max_length=11, null=True, unique=True) avatar = models.FileField(upload_to="avatars/", default="avatars/default.png") create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True) blog = models.OneToOneField(to="Blog", to_field="nid", null=True,on_delete=models.CASCADE) #视图文件的代码 def reg(request): """注册账号用户""" if request.is_ajax(): reg_info = reg_form(request.POST) response={"user":None,"message":None} if reg_info.is_valid(): response["user"]=reg_info.cleaned_data.get("reg_account") user = reg_info.cleaned_data.get("reg_account") pwd = reg_info.cleaned_data.get("pwd") email = reg_info.cleaned_data.get("email") avatar = request.FILES.get("avatar") # 如果账号密码通过了局部钩子和全局钩子,就把注册信息写入到数据库中 if avatar: # 要用create_user方法,会进行加密存储 UserInfo.objects.create_user(username=user,password=pwd,email=email,avatar=avatar) else: #考虑到avatar不是必填项,用户如果没有上传头像,则Userinfo表不添加,不添加则会使用字段设置的defalut的内容 UserInfo.objects.create_user(username=user, password=pwd, email=email) else: response["message"] = reg_info.errors return JsonResponse(response) new_regs = reg_form() return render(request, "reg.html", {"new_regs": new_regs}) #视图文件的代码的部分优化内容 create_user 函数是可以传字典的,那么我们可以设置空字典,has_avatar={} 如果有avatar字段,则has_avatar={"avatar":avatar} avatar = request.FILES.get("avatar") has_avatar={} if avatar: has_avatar["avatar"]=avatar UserInfo.objects.create_user(username=user,password=pwd,email=email,**has_avatar) ```
-
avatar自定义路径配置以及media配置解耦上传路径
Django有2中静态文件,一个是/static/需要给浏览器下载的 ,另一个是/media/由用户上传的.
和static一样,我们也需要把avatar上传的文件做下解耦,单独放到一个文件夹去,
我们可以去setting.py配置MEDIA_ROOT字段来实现这样的目的,通过下面设置,上传的文件路径就变成了cnblogs这个应用的upload_folder文件夹,完整的路径为
cnblogsupload_folderavatars1.jpg
MEDIA_ROOT = os.path.join(BASE_DIR,"cnblogs/upload_folder/")
-
avatar自定义路径放到每个用户自己的username下
考虑每个用户上传的文件命名可能重名,最好还是将avatar文件放到用户名下的路径
在model中自定义一个函数,然后用upload_to去接受这个函数返回值
在这个自定义函数中也可以对上传的filename进行重命名,防止任意路径访问漏洞
def user_directory_path(instance,filename): print("instance",instance) print("filename",filename) return os.path.join(instance.username,"avatars",filename) class UserInfo(AbstractUser): # avatar = models.FileField(upload_to="avatars/", default="avatars/default.png") avatar = models.FileField(upload_to=user_directory_path, default="avatars/default.png")
-
media配置访问url路由
上一步我们配置了上传的路径,但是里面的客户上传的文件内容或者图片要进行url访问呢?之前的static是django默认配置好了访问路由.那么media我们需要做如下配置
1.1 setting.py 设置
MEDIA_URL = "/media/"
1.2 路由导入,在urls.py需要导入
from Blog import settings from django.views.static import serve
1.3 设置路由, media是1.1随意命名的,这个设置好后media就代表了绝对路径
cnblogs/upload_folder/
这么设置就表示但凡绝对路径是cnblogs/upload_folder/下的子文件夹,都开通了访问路由.
re_path("media/(?P<path>.*)/$",serve,{"document_root":settings.MEDIA_ROOT})
-
设置博客导航
基于bootstrap修改主页样式,用到了导航条,设置了文章类型:随笔 新闻 日记 右侧增加个人信息 如果登陆>>显示用户名字,用户头像,以及下拉菜单(注销,改头像,改密码) 如果未登陆>>显示注册,登陆按钮
-
博客body部分,分为左中右,分别列宽占比4-6-2
用超级账户登陆,先随机导入点数据.刚进去的管理员后台是这样的,看不到之前创建的数据库表
我们需要在应用名/admin.py把ORM的model表进行注册并导入进来,
from cnblogs import models admin.site.register(models.UserInfo) admin.site.register(models.Category) admin.site.register(models.Tag) admin.site.register(models.ArticleUpDown) admin.site.register(models.Artical) admin.site.register(models.Artical2Tag) admin.site.register(models.Comment) admin.site.register(models.Blog)
刷新admin页面就可以看到各个数据库表,接着进行后台数据的自定义添加.  添加的时候最好从大到小的顺序添加 比如: 以User表开始,从用户>用户的Bolgs>Article>(Catagory)(Tag)(Artical2Tag)>Comment 还有注意表和表之间的关系,比如创建一个user,然后创建该用户的Bolgs信息.在数据库中,user和Bolgs是一对一的关系.所以创建完毕后在user里要去绑定这个关系,其他的表的关系也是如此 
-
ORM获取数据库所有的文章,渲染主页中中间文章的内容
-
然后做个人主页,配置路由
re_path('^index/(?P<user_home_site>w+)/$', user_home_site)
,可以先配置404错误网页,如果个人ID未找到显示404页注意:判断个人ID是否存在用exists比上面的查询效率高,
UserInfo.objects.filter(username=user).exists()
但是如果考虑到user对象是用来查博客的个人站点,基本存在的可能性高于不存在,并且需要根据user对象对博文,分类进行渲染.所以实际还是用这个效率高
UserInfo.objects.filter(username=user).first()
-
个人主页的数据渲染
做个人主页,大致框架分左右两边,左边 查询当前站点每个分类以及对应的文章数artical_num_each_categoryies = Category.objects.filter(blog=user_blog).values("pk").annotate( c=Count("artical__nid")).values("title", "c")
查询当前站点每个标签以及对应的文章数
artical_num_each_tags = Artical.objects.filter(user=user_check).values("pk").annotate(c=Count("tag__nid")).values( "title", "tag__title", "c")
查询当前站点每一个年月的名称以及对应的文章数
这里要实现的效果是 2012 6 月(10),根据年月来进行聚合.但是实际上我们存到数据库的时候是
2021-10-26 16:16:41.691016
这种格式,因为精确到毫秒级别,所以每个时间都是不同的,无法聚合.在不对原有的数据库进行修改的前提下,要时间聚合思路有2个,
一个是根据mysql语句对时间进行格式化
数据库中可以用date_format对时间的显示做输出格式化. mysql> select * from date_time; +------+---------------------+----------+------------+ | id | dt | t | d | +------+---------------------+----------+------------+ | 1 | 2021-10-30 10:54:44 | 10:54:44 | 2021-10-30 | | 2 | 2021-10-30 10:54:49 | 10:54:49 | 2021-10-30 | +------+---------------------+----------+------------+ 2 rows in set (0.00 sec) mysql> select date_format(dt,"%Y-%m-%d") from date_time; +----------------------------+ | date_format(dt,"%Y-%m-%d") | +----------------------------+ | 2021-10-30 | | 2021-10-30 | +----------------------------+
另一个就是通过orm传输SQL语法进行格式化,这里需要用extra方法
extra(select=None,where=None,params=None,tables=None,order_by=None,select_params=None)
完整格式query_set对象.extra(select="key: 'sql语句' ")
Artical.objects.extra(select={"filter_ret":"date_format(create_time,'%%Y-%%m-%%d')"}).values("title","filter_ret") 有时候ORM查询语法无法表达复杂的查询语句,我们可以通过extra指定一个或者多个参数,例如select,where,tables,这些参数都不是必须,但是必须要有一个, 以select为例,我们在数据库的artical表中有个文章的create_time字段,我们利用extra进行时间过滤 ret=models.Artical.object.extra(select={"filter_ret":"create_time > '202-10-26' "})
extra的详细案例
Django里关于时间的筛选有内置的Trunc方法 ,导包和实例如下:
from django.db.models.functions import TruncDate, TruncDay, TruncHour, TruncMinute, TruncSecond Experiment.objects.annotate( ... date=TruncDate('start_datetime'), ... day=TruncDay('start_datetime', tzinfo=melb), ... hour=TruncHour('start_datetime', tzinfo=melb), ... minute=TruncMinute('start_datetime'), ... second=TruncSecond('start_datetime'), ... ).values('date', 'day', 'hour', 'minute', 'second').get() {'date': datetime.date(2014, 6, 15), 'day': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=<DstTzInfo 'Australia/Melbourne' AEST+10:00:00 STD>), 'hour': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=<DstTzInfo 'Australia/Melbourne' AEST+10:00:00 STD>), 'minute': 'minute': datetime.datetime(2014, 6, 15, 14, 30, tzinfo=<UTC>), 'second': datetime.datetime(2014, 6, 15, 14, 30, 50, tzinfo=<UTC>)
在实际应用中我们对于 2021-06-10 15:30:5657这种datetime时间格式分别用extra和自带的TruncDate进行格式化
# 第一种,extra格式化 artical_ret = user_articals.extra(select={"filter_ret":"create_time > '2021-10-26 16:20' "}) date_list=Artical.objects.filter(user=user_check).extra(select={"strift_date":"date_format(create_time,'%%Y-%%m')"}).values("strift_date").annotate(c=Count("nid")).values("strift_date","c")[0] print(date_list) #{'strift_date': '2021-10', 'c': 2} # 第二种,自带的TruncMonth方法 date_list2=Artical.objects.filter(user=user_check).annotate(month=TruncMonth("create_time")).values("month").annotate(c=Count("nid")).values("month","c").first() print(date_list2) #{'month': datetime.datetime(2021, 10, 1, 0, 0), 'c': 1} print(datetime.datetime.strftime(date_list2["month"],"%Y-%m")) #2021-10
-
-