Django 博客园练习--待完成

项目流程:

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页面就可以看到各个数据库表,接着进行后台数据的自定义添加.
      
      ![](https://img2020.cnblogs.com/blog/1610779/202111/1610779-20211102173402490-604225317.png)
      
      
      添加的时候最好从大到小的顺序添加  比如:
      
      以User表开始,从用户>用户的Bolgs>Article>(Catagory)(Tag)(Artical2Tag)>Comment
      
      还有注意表和表之间的关系,比如创建一个user,然后创建该用户的Bolgs信息.在数据库中,user和Bolgs是一对一的关系.所以创建完毕后在user里要去绑定这个关系,其他的表的关系也是如此
      
      ![](https://img2020.cnblogs.com/blog/1610779/202111/1610779-20211102173459049-1131097736.png)
      
      • 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
        

4. 功能测试

5.项目部署

原文地址:https://www.cnblogs.com/Young-shi/p/15391299.html