• django bbs项目


    BBS(Bulletin Board Service,公告牌服务)是Internet上的一种电于信息服务系统。它提供一块公共电子白板,每个用户都可以在上面书写,可发布信息或提出看法。

    注:这里只对主要功能进行实现,其中还有很多小功能未实现,有小bug未调试。

    前期准备

    准备一个新的django项目,数据库配置为mysql的bbs库。
    templates、static目录配置。

    表设计
    一个项目中,表设计环节是最重要的。
    其中:分类表的设定是一个文章只属于一类。

    建表

    模型层 app01\models.py

    点击查看代码
    from django.db import models
    from django.contrib.auth.models import AbstractUser
    from django.forms import BooleanField
    # Create your models here.
    
    # 用户表 新增4个字段
    class UserInfo(AbstractUser):
        phone = models.CharField(max_length=11,verbose_name='手机号')
        avatar = models.FileField(upload_to="avatar/",default='vatar/default.png',verbose_name='头像')
        '''
        给vatar字段传文件对象,文件会自动存储到 avatar/ 下,然后vatar字段只保存文件路径 vatar/xxx.png
        不传默认是vatar/default.png
        '''
        create_time = models.DateField(auto_now_add=True,verbose_name='注册时间')
        
        blog = models.OneToOneField(to='Blog',null=True)
    
    # 个人站点表
    class Blog(models.Model):
        site_name = models.CharField(max_length=32,verbose_name='站点名称')
        site_title = models.CharField(max_length=32,verbose_name='站点标题')
        site_theme = models.CharField(max_length=64,verbose_name='站点样式') #存css/js的文件路径
        
    # 文章分类表
    class Category(models.Model):
        name = models.CharField(max_length=32,verbose_name='文章类名')
        blog = models.ForeignKey(to='Blog',null=True)
    
    # 文章标签表
    class Tag(models.Model):
        name = models.CharField(max_length=32,verbose_name='文章标签名')
        blog = models.ForeignKey(to='Blog',null=True)
    
    # 文章表
    class Article(models.Model):
        title = models.CharField(max_length=32,verbose_name='文章标题')
        desc = models.CharField(max_length=255,verbose_name='文章简介')
        content = models.TextField(verbose_name='文章内容')
        create_time = models.DateField(auto_now_add=True,verbose_name='创建时间')
    
        #数据库字段优化设计 不直接统计对应点赞表中的赞数,而是每一次点赞 +1
        up_num = models.BigIntegerField(verbose_name='点赞数',default=0)
        dowm_num = models.BigIntegerField(verbose_name='点踩数',default=0)
        comment_num= models.BigIntegerField(verbose_name='评论数',default=0)
        
        blog = models.ForeignKey(to='Blog',null=True)
        category = models.ForeignKey(to='Category',null=True)
    
        # 半自动的方式创建多对多关系
        tags = models.ManyToManyField(to='Tag',
                                    through='ArticleToTag',
                                    through_fields=('article','tag'),
                                    )
    class ArticleToTag(models.Model):
        article = models.ForeignKey(to='Article')
        tag = models.ForeignKey(to='Tag')
    
    # 点踩点赞表
    class UpAndDown(models.Model):
        user = models.ForeignKey(to='UserInfo')
        article = models.ForeignKey(to='Article')
        is_up = BooleanField()
    
    class Comment(models.Model):
        user = models.ForeignKey(to='UserInfo')
        article = models.ForeignKey(to='Article')    
        content = models.CharField(max_length=255,verbose_name='评论内容')
        comment_time = models.DateTimeField(auto_now_add=True,verbose_name='评论时间') 
        # 自关联  此字段用来记录子评论对于的父评论
        parent = models.ForeignKey(to='self',null=True,verbose_name='父评论')
    

    将默认的用户头像放在 用户表的vatar字段定义的位置 avatar\default.png

    配置文件中加上 AUTH_USER_MODEL = 'app01.UserInfo',声明user表

    执行两条数据库迁移命令,创建定义好的表。

    用户注册

    - forms组件
    - 用户头像实时展示
    - ajax 提交注册信息以及展示提示信息
    
    
    1. 注册相关的forms组件
       不同功能代码需要解耦合。
       如果你的项目只用到一个forms组件,你可以直接新建一个py文件(eg:myforms.py)书写forms相关的自定义类;
       如果你用到多个forms组件,你可以创建一个文件夹,再在文件夹内根据forms组件功能的不同创建不同的py文件。
    
    2. 利用form组件渲染前端标签
       1) 不用form表单提交,用ajax
       2) 利用form标签获取到用户输入数据
       $('#myform').serializeArray()
       form标签的jquery对象.serializeArray()  可以拿到form表单中传输的普通键值对组成的列表(不包括文件)。
       [{name: 'username', value: 'yxf'}, {name: 'password', value: 'xxx'}}, {…}, {…}]
    
    3. 手动渲染用户头像标签
       label标签中的所有内容(包括图片),都能绑定对应的input标签
       <label for="id_avatar">
       头像:
       <img src="{% static 'img/default.png' %}" alt="" id='myimg' style=" 100px;margin-left: 10px;">
       </label>
       <input type="file" id="id_avatar" style="display:none">
    
    4. 实时展示用户头像
       1) .change() change事件某个标签值发送变化时触发,例如input标签上传文件时触发
       2) 利用到了文件阅读器,需要注意myFileReaderObj.readAsDataURL(fileObj)是异步操作
       3) .onload()  onload事件会等待加载完毕才会触发
     
        $('#id_avatar').change(function(){
            // 文件阅读器对象
            // 1. 生成一个文件阅读器
            let myFileReaderObj = new FileReader();
            // 2. 获取用户上传的头像文件
            let fileObj = $(this)[0].files[0];
            // 3. 将文件对象交给阅读器处理   
            myFileReaderObj.readAsDataURL(fileObj)  // 异步 IO操作 所以这里文件还没读完就会执行下一行代码
            // 4. 利用阅读器将文件展示到前端页面   
            myFileReaderObj.onload = function(){  // onload 等待myFileReaderObj对象的任务都执行完成才执行
                $('#myimg').attr('src',myFileReaderObj.result)
            }
        })
    
    5. 一旦用户信息不合法,如何精确地渲染提示
       1) form组件渲染的input标签都有id值: id_字段名 
          .auto_id可以获取到对应的id值
          <label for="{{ form.auto_id }}">{{form.label}}:</label>
       2) 根据后端返回的字段及报错信息可以手动拼接对应的input标签的id,以定位到span提示信息的标签
       3) 提示功能的完善:
          jQuery链式操作 展示提示信息;添加 has-error类,使输入框变红
          input 框获取焦点时 触发focus事件,去重对应的红色边框与提示信息
    

    效果:

    image

    项目结构展示

    注册代码:

    路由层 BBS\urls.py

    点击查看代码
    from django.conf.urls import url
    from django.contrib import admin
    from app01 import views
    urlpatterns = [
        url(r'^admin/', admin.site.urls),
        # 用户注册
        url(r'^register/',views.register,name='register'),
        # 用户登录
        url(r'^login/',views.login,name='login')
    ]
    

    视图层

    app01\myform.py

    点击查看代码
    from django import forms
    from app01 import models
    class Myform(forms.Form):
        username = forms.CharField(label='用户名',max_length=8,min_length=3,
                                error_messages={
                                    'required':'用户名不能为空',
                                    'max_length':'用户名不能超过8位',
                                    'min_length':'用户名不能少于3位',
                                },
                                widget = forms.widgets.TextInput(attrs={'class':'form-control'}),
                                )
        password = forms.CharField(label='密码',max_length=8,min_length=3,
                                error_messages={
                                    'required':'密码不能为空',
                                    'max_length':'密码不能超过8位',
                                    'min_length':'密码不能少于3位',
                                },
                                widget = forms.widgets.PasswordInput(attrs={'class':'form-control'}),
                                )
        confirm_password = forms.CharField(label='确认密码',max_length=8,min_length=3,
                                error_messages={
                                    'required':'确认密码不能为空',
                                    'max_length':'确认密码不能超过8位',
                                    'min_length':'确认密码不能少于3位',
                                },
                                widget = forms.widgets.PasswordInput(attrs={'class':'form-control'}),
                                )
        email = forms.EmailField(label='邮箱',
                                error_messages={
                                    'required':'邮箱不能为空',
                                    'invalid':'邮箱格式不正确',
                                },
                                widget = forms.widgets.EmailInput(attrs={'class':'form-control'}),
                                )
    
        # 局部钩子校验用户名是否已存在
        def clean_username(self):
            username = self.cleaned_data.get('username')
            is_exist = models.UserInfo.objects.filter(username=username)
            if is_exist:
                self.add_error('username','用户名已存在')
            return username
    
        #全局钩子校验两次密码是否一致
        def clean(self):
            password = self.cleaned_data.get('password')
            confirm_password = self.cleaned_data.get('confirm_password')
            if not password == confirm_password:
                self.add_error('confirm_password','两次密码不一致')
            return self.cleaned_data
    

    app01\views.py

    点击查看代码
    from django.http import HttpResponse, JsonResponse
    from django.shortcuts import render,reverse
    from app01.myform import Myform
    from app01 import models
    
    # Create your views here.
    
    def register(request): 
        form_obj=Myform()
        back_dic = {'code':1000}
        if request.is_ajax():
            form_obj=Myform(request.POST)
            if not form_obj.is_valid():
                back_dic['code'] = 2000
                back_dic['msg'] = form_obj.errors
                #form_obj.errors => {'password': ['密码不能为空'], 'confirm_password': ['确认密码不能为空'], ...}}
            else:
                clean_data = form_obj.cleaned_data
                clean_data.pop('confirm_password')
    
                avatar = request.FILES.get('avatar')
                if avatar:
                    clean_data['avatar'] = avatar
                #clean_data是字典形式, **clean_data能打散字典进行传参
                models.UserInfo.objects.create_user(**clean_data)
                back_dic['url'] = reverse('login')
    
            return JsonResponse(back_dic)
        return render(request,'register.html',locals())
    
    
    def login(request):
        return HttpResponse('login')
    

    模板层 templates\register.html

    点击查看代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script>
        {% load static %}
        <link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
        <script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
        <script src="{% static '/js/mysetup.js' %}"></script>
        <title>BBS</title>
    </head>
    <body>
        <div class="container">
            <div class="row">
                <div class="col-md-12">
                    <h2 class="text-center">注册页面</h2>
                    {% csrf_token %}
                    <form action="" id="myform">
                            {% for form in form_obj %}
                            <div class="form-group">
                                <!-- {{ form.auto_id }} 可以获取到input标签对应的id -->
                                <label for="{{ form.auto_id }}">{{form.label}}:</label>
                                {{form}}
                                <span style="color:red" class="pull-right"></span>
                            </div>
                            {% endfor %}
                            <br>
                            
                        <label for="id_avatar">
                            头像:
                            <img src="{% static 'img/default.png' %}" alt="" id='myimg' style=" 100px;margin-left: 10px;">
                        </label>
                        <input type="file" id="id_avatar" style="display:none">
                        <br><br>
                        <input type="button" id="id_commit" class="btn form-control btn-primary" value="提交">
                    </form>
                </div>
            </div>
        </div>
    
        <script>
            // 将前端页面的默认头像展示为用户上传的头像
                // .change(function(){}) 当某个标签值发送变化时触发
            $('#id_avatar').change(function(){
                // 文件阅读器对象
                // 1. 生成一个文件阅读器
                let myFileReaderObj = new FileReader();
                // 2. 获取用户上传的头像文件
                let fileObj = $(this)[0].files[0];
                // 3. 将文件对象交给阅读器处理   
                myFileReaderObj.readAsDataURL(fileObj)  // 异步 IO操作 所以这里文件还没读完就会执行下一行代码
                // 4. 利用阅读器将文件展示到前端页面   
                myFileReaderObj.onload = function(){  // onload 等待myFileReaderObj对象的任务都执行完成才执行
                    $('#myimg').attr('src',myFileReaderObj.result)
                }
            })
    
    
            $('#id_commit').click(function(){
                // console.log($('#myform').serializeArray())  
                // 打印数组包字典的形式 [{name: 'username', value: 'yxf'}, {name: 'password', value: 'xxx'}}, {…}, {…}]
                formDataObj = new FormData();
                $('#myform').serializeArray().forEach(function(i){formDataObj.append(i.name,i.value)})
                formDataObj.append('avatar',$('#id_avatar')[0].files[0])
                $.ajax({
                    url:'',
                    data: formDataObj,
                    method: 'post',
                    contentType: false,
                    processData: false,
                    success: function(args){
                        if (args.code==1000){window.location.href=args.url}
                        else {
                            console.log(args.code)
                            let msg=args.msg
                            for (var i in msg) {
                                // console.log(i,msg[i][0])  // username 用户名不能为空
                                let id = '#id_' + i
                                // 为span空标签添加提示语,再将提示框变为红色
                                $(id).next('span').text(msg[i][0]).parent().addClass('has-error')
    
                            }
                        }
                    }
                })
    
                $('input').focus(function(){
                    // 当提示框获取到焦点时,提示框去掉红色边框效果 以及不再展示提示语
                    $(this).next('span').text('').parent('div').removeClass('has-error')
                })
            })
        </script>
    </body>
    </html>
    

    将默认头像放到static\img\default.png供前端页面展示;
    ajax使用以引入文件static\js\mysetup.js的方式使用csrf中间件。(ajax使用csrf)
    bootstrap资源放到static\others\bootstrap-3.4.1,供前端页面调用

    用户登录

    实现两大功能:
    /login     登录
    /get_code  图片验证码
    
    - auth模块 实现用户信息校验 与登录状态保存
    - ajax 提交登录信息 与 展示提示信息
    
    1. 图片验证码如何展示
      注:
       img标签的src属性可以写三种类型:
       a) 图片url (完整的url https://xx.xx.com/xx 与当前网页的url /get_code/)
       b)图片路径 
       c)图片二进制数据
       
      1) 借助pillow模块 Image ImageDraw ImageFont
      2) 借助内存管理器io模块 BytesIo 临时存储图片
      3) 产生随机验证码
        def random_str():
            random_upper = chr(random.randint(65,90))
            random_lower = chr(random.randint(97,122))
            random_int = str(random.randint(0,9))
            return random.choice([random_upper,random_lower,random_int])
        在session中保存验证码用于后续校验
    
      4) 产生随机颜色
         def random_color():
            return (random.randint(0,255),random.randint(0,255),random.randint(0,255))
    
      5) 当验证码看不清时如何重载
        img标签的src属性改变时,会触发图片的重载。
        可以在点击看不清按钮时,触发更改img的src属性值,从而实现验证码图片的重载
    
      6) 使用事务绑定 评论表新增数据 以及 文章表评论数+1
    

    效果展示:

    登录代码

    路由层 BBS\urls.py

    点击查看代码
        # 用户登录
        url(r'^login/',views.login,name='login'),
        # 图片验证码
        url(r'^get_code/',views.get_code,name='getCode'),
    

    视图层 app01\views.py

    点击查看代码
    from django.contrib import auth
    
    def login(request):
        back_dic = {'code':1000}
        if request.method == 'POST':
            username = request.POST.get('username')
            password = request.POST.get('password')
            code = request.POST.get('code')
            # 核对用户名密码是否正确
            user_obj = auth.authenticate(request,username=username,password=password)
            if user_obj:
                if code.upper() == request.session.get('code').upper():
                    #保存该用户登录状态
                    auth.login(request,user_obj)
                    back_dic['url'] = reverse('home')
                else:
                    back_dic['code'] = 2000
                    back_dic['msg'] = '验证码输入错误'
            else:
                back_dic['code'] = 3000
                back_dic['msg'] = '用户名或密码输入错误'
            return JsonResponse(back_dic)
        return render(request,'login.html')
    
    from PIL import Image,ImageDraw,ImageFont
    from io import BytesIO
    import random
    
    def get_code(request):
        '''生成验证码图片'''
        def random_color():
            '''生成随机颜色'''
            return (random.randint(0,255),random.randint(0,255),random.randint(0,255))
    
        def random_str():
            '''大写字母/小写字母/数字 产生一个随机字符'''
            random_upper = chr(random.randint(65,90))
            random_lower = chr(random.randint(97,122))
            random_int = str(random.randint(0,9))
            return random.choice([random_upper,random_lower,random_int])
    
        img_obj = Image.new('RGB',(300,35),random_color())
        img_draw = ImageDraw.Draw(img_obj)
        img_font = ImageFont.truetype('static/font/impact.ttf',28)
    
        code = ''
        for i in range(5):
            randomStr = random_str()
            img_draw.text((i*50+50,0),randomStr,random_color(),img_font)
    
            code += randomStr
        
        request.session['code'] = code
        io_obj = BytesIO()
        img_obj.save(io_obj,'png')
        return HttpResponse(io_obj.getvalue())
    

    字体文件可直接拷贝 C:\Windows\Fonts下的文件

    image

    模板层 templates\login.html

    点击查看代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script>
        {% load static %}
        <link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
        <script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
        <script src="{% static '/js/mysetup.js' %}"></script>
        <title>BBS</title>
    </head>
    <body>
        <div class="container">
            <div class="row">
                <h2 class="text-center">登陆页面</h2>
                <form action="">
                    <div class="form-group">
                        <label for="id_username">用户名:</label>
                        <input type="text" id="id_username" class="form-control">
                    </div>
                    <div class="form-group">
                        <label for="id_password">密码:</label>
                        <input type="password" id="id_password" class="form-control">
                    </div>
            
                    <div class="form-group">
                        <label for="">验证码:</label>
                        <div class="row">
                            <div class="col-md-3">
                                <input type="text" id="id_code" class="form-control">
                            </div>
                            <div class="col-md-3">
                                <img src="{% url 'getCode' %}" alt="" id="id_img">
                            </div>
                            <div class="col-md-3">
                                &nbsp;
                                <input type="button" id="id_changeImg" class="btn btn-warning" value="看不清">
                            </div>
                        </div>
                    </div>
                    <div class="form-group">
                        <input type="button" id="id_commit" class="btn btn-primary" value="登录">
                        &nbsp;&nbsp;<span id="id_prompt" style="color: red;"></span>
                    </div>
    
                </form>
            </div>
        </div>
    
    <script>
        // 点击看不清按钮刷新验证码图片 img标签的src改变时会重载图片
        $('#id_changeImg').click(function(){
            let src = $('#id_img').attr('src')+'?'
            $('#id_img').attr('src',src)
        })
    
        $('#id_commit').click(function(){
            $.ajax({
                url: '',
                method: 'post',
                data: {'username':$('#id_username').val(),'password':$('#id_password').val(),'code':$('#id_code').val()},
    
                success: function(args){
                    if(args.code==1000){
                        window.location.href = args.url
                    }
                    else{
                        $('#id_prompt').text(args.msg)
                    }
                },
    
            })
        })
    </script>
    </body>
    </html>
    

    bbs首页搭建

    首页url /home

    顶部导航条

    实现效果:
    当用户未登录时展示 登录与注册

    当用户登录时展示用户名与更多操作

    代码:

    路由层 BBS\urls.py

    点击查看代码
        # 首页
        url(r'^home/',views.home,name='home'),
    

    视图层 app01\views.py

    点击查看代码
    def home(request):
        return render(request,'home.html',locals())
    

    模板层 templates\home.html

    拷贝bootstrap的导航栏代码,颜色改为反色导航条

    点击查看home.html 导航条雏形代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script>
        {% load static %}
        <link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
        <script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
        <script src="{% static '/js/mysetup.js' %}"></script>
        <title>BBS</title>
    </head>
    <body>
        <!-- 导航条 -->
        <nav class="navbar navbar-inverse">
            <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="#">BBS</a>
              </div>
          
              <!-- Collect the nav links, forms, and other content for toggling -->
              <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                  <li class="active"><a href="#">博客 <span class="sr-only">(current)</span></a></li>
                  <li><a href="#">文章</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="#">Action</a></li>
                      <li><a href="#">Another action</a></li>
                      <li><a href="#">Something else here</a></li>
                      <li role="separator" class="divider"></li>
                      <li><a href="#">Separated link</a></li>
                      <li role="separator" class="divider"></li>
                      <li><a href="#">One more separated link</a></li>
                    </ul>
                  </li>
                </ul>
                <form class="navbar-form navbar-left">
                  <div class="form-group">
                    <input type="text" class="form-control" placeholder="Search">
                  </div>
                  <button type="submit" class="btn btn-default">Submit</button>
                </form>
                <ul class="nav navbar-nav navbar-right">
                    <!-- 用if判断登录状态实现登录前后的不同内容展示 -->
                    {% if request.user.is_authenticated %}
                            <li><a href="">{{ request.user.username }}</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="#">修改密码</a></li>
                                <li><a href="#">更换头像</a></li>
                                <li><a href="#">后台管理</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="#">退出登录</a></li>
                              </ul>
                            </li>
                    {% else %}  
                        <li><a href="/login/">登录</a></li>
                        <li><a href="/register/">注册</a></li>    
                    {% endif %}
                    
                    </ul>
                  </li>
                </ul>
              </div><!-- /.navbar-collapse -->
            </div><!-- /.container-fluid -->
          </nav>
    </body>
    </html>
    

    修改密码

    1. 入口: 登陆后点击更多信息中的更改密码
    2. 绑定更改密码标签与模态框,点击时弹出模态框可输入信息
    3. 提交信息使用ajax
    5. 后端对输入的内容进行比对,有误则展示到模态框,正确则刷新页面
     
    

    效果:

    代码:

    使用login_required装饰器设置此路由为登陆状态可访问,此处全局配置,配置非登录状态访问跳转/login/

    settings.py

    LOGIN_URL = '/login/'
    

    路由层 BBS\urls.py

    点击查看代码
        # 修改密码
        url(r'^set_password/',views.set_password,name='setPassword'),
    

    视图层 app01\views.py

    点击查看代码
    from django.contrib.auth.decorators import login_required
    
    @login_required
    def set_password(request):
        back_dic = {'code':1000}
        if request.method == 'POST':
            old_password = request.POST.get('old_password')
            new_password = request.POST.get('new_password')
            confirm_password = request.POST.get('confirm_password')
            if request.user.check_password(old_password):
                if new_password == confirm_password:
                    request.user.set_password(new_password)
                    request.user.save()
                    back_dic['msg'] = '修改成功'
                else:
                    back_dic['code'] = 1002
                    back_dic['msg'] = '两次密码不一致'
            else:
                back_dic['code'] = 1001
                back_dic['msg'] = '原密码错误'
        return JsonResponse(back_dic)
    

    模板层 templates\home.html
    拷贝bootstrap大模态框代码进行更改

    点击查看修改密码代码
        ...
                                <!-- 添加 data-toggle="modal" data-target=".bs-example-modal-lg属性后点击此标签弹出模态框 -->
                                <li><a href="" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
        ...
        <!-- Large modal 大模态框 点击修改密码弹出 将data-toggle="modal" data-target=".bs-example-modal-lg"加到点击密码的a标签-->
        <!-- <button type="button" class="btn btn-primary" data-toggle="modal" data-target=".bs-example-modal-lg" >Large modal</button> -->
        <!-- 这里的class中还需要加上bs-example-modal-lg才能弹出 -->
        <div class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
          <div class="modal-dialog modal-lg" role="document">
            <div class="modal-content">
                <h2 class="text-center">修改密码</h2>
                <div class="row">
                    <div class="col-md-8 col-md-offset-2">
                        <div class="form-group">
                            <label for="username">用户名</label>
                            <input type="text" class="form-control" id="username" value="{{ request.user.username }}" disabled>
                          </div>
                          <div class="form-group">
                            <label for="old_password">原密码</label>
                            <input type="password" class="form-control" id="old_password">
                          </div>
                          <div class="form-group">
                            <label for="new_password">新密码</label>
                            <input type="password" class="form-control" id="new_password">
                          </div>
                          <div class="form-group">
                            <label for="confirm_password">确认密码</label>
                            <input type="password" class="form-control" id="confirm_password">
                          </div>
                        <div class="modal-footer">
                            <span class="pull-left" id="prompt" style="color: red;"></span>
                            <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                            <button type="button" class="btn btn-primary" id="change_pwd_btn">确认</button>
                        </div>
                    </div>
                </div>
            </div>
          </div>
        </div>
    
        <script>
            // 修改密码使用ajax提交信息
            $('#change_pwd_btn').click(function(){
                $.ajax({
                    url: "{% url 'setPassword' %}",
                    method: 'post',
                    data: {'old_password':$('#old_password').val(),'new_password':$('#new_password').val(),'confirm_password':$('#confirm_password').val()},
                    success: function(args){
                        if (args.code == 1000){
                            $('#prompt').text(args.msg).attr('style','color: blue');
                            window.location.reload()
                            }
                        else {$('#prompt').text(args.msg)}
                    }
                })
            })
        </script>
    

    退出登录

    1. 入口: 登陆后点击更多信息中的退出登录
    2. auth.logout(request) 清除本地与浏览器登录状态的session
    

    效果:

    代码:

    路由层 BBS\urls.py

    点击查看代码
        # 退出登录
        url(r'^logout/',views.logout,name='logout'),
    

    视图层 app01\views.py

    点击查看代码
    @login_required
    def logout(request):
        auth.logout(request)
        return redirect(reverse('home'))
    

    模板层 templates\home.html

    点击查看退出登录代码
                                <!-- 添加退出登录的url -->
                                <li><a href="{% url 'logout' %}">退出登录</a></li>
    

    **admin后台管理

    django 提供了一个可视化界面 /admin 方便程序员对模型表进行数据的增删改查。
    1. 创建超级用户
    
    2. 模型表注册:
    首先要在admin.py中注册你的模型表,告诉admin你需要操作哪些表,注册完才会在后台管理页面显示该表,且表名后会自动加‘s’
    #admin.py
    from django.contrib import admin
    from app01 import models
    # Register your models here.
    admin.site.register(models.UserInfo)
    admin.site.register(models.Blog)
    admin.site.register(models.Article)
    admin.site.register(models.Category)
    admin.site.register(models.Tag)
    admin.site.register(models.ArticleToTag)
    admin.site.register(models.UpAndDown)
    admin.site.register(models.Comment)
    
    3. 后台管理页面展示中文表名
    默认后台管理页面展示models中定义的英文表名,想要展示中文表名可在models.py中定义
    class Category(models.Model):
        name = models.CharField(max_length=32,verbose_name='文章类名')
        blog = models.ForeignKey(to='Blog',null=True)
    
        class Meta:   
            # verbose_name = '文章分类表'  #修改admin后台表名展示为 文章分类表s
            verbose_name_plural = '文章分类表'  #修改admin后台表名展示为 文章分类表
    
    class Tag(models.Model):
        name = models.CharField(max_length=32,verbose_name='文章标签名')
        blog = models.ForeignKey(to='Blog',null=True)
    
        class Meta:
            # verbose_name = '文章标签表'  #修改admin后台表名展示为 文章标签表s
            verbose_name_plural = '文章标签表'  #修改admin后台表名展示为 文章标签表
    
    4. 增删改查
    django会给每一个注册了的模型表自动生成增删改查4条url
    http://127.0.0.1:8000/admin/app01/userinfo(表名)/                   # 查
    http://127.0.0.1:8000/admin/app01/userinfo(表名)/add/               # 增
    http://127.0.0.1:8000/admin/app01/userinfo(表名)/5(主键值)/change/   # 改
    http://127.0.0.1:8000/admin/app01/userinfo(表名)/5(主键值)/delete/   # 删
    

    录入数据

    http://127.0.0.1:8000/admin
    
    从文章表入手创建blog与category对象
    创建文章标签表对象
    去Article to tag表绑定文章与标签
    去user表绑定与blog的一对一关系(phone未填写会报错,可填写或配置blank=True)
    


    添加双下str方法后,展示为

    admin后台管理页面录入数据时,有null=True的字段,这里不填还是会报错,需要加上blank=True

    phone = models.CharField(max_length=11,verbose_name='手机号',null=True,blank=True)
    
    null=True    表示数据库改字段可以为空
    blank=True   表示admin后台管理录入数据该字段可以为空
    

    **django暴露指定文件夹

    需求:暴露用户头像资源

    1. 将用户上传的文件放到指定文件夹 项目根目录/media/下
    2. 开放media下的用户头像资源,使 127.0.0.1:8000/media/avatar/bbb.png  可以访问到项目根目录/media/avatar/bbb.png
    

    配置:

    1. BBS\settings.py添加配置
    
    #配置用户上传的文件的存储位置(这里的目录名可自行定义)
    '''
    配置好后用户上传的文件默认放到此目录下,例如之前定义的
    avatar = models.FileField(upload_to="avatar/",default='vatar/default.png',verbose_name='头像')
    头像默认放到 根目录的avatar下,定义了此配置后,头像放到 根目录/media/下的avatar下
    '''
    
    MEDIA_ROOT = os.path.join(BASE_DIR,'media')
    
    2. BBS\urls.py 添加路由
    
    from django.views.static import serve
    from BBS import settings
    
        # 暴露指定文件夹资源 media,固定写法,特别是(?P<path>.*)后面不能加/
        url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}),
    
    3. 访问测试:
    之前创建的项目根目录下的avatar目录直接移到media下,即头像的路径为 media\avatar\111.png,。
    可通过此链接访问 http://127.0.0.1:8000/media/avatar/111.png 
    

    主页面展示

    1. 主页面/home282布局,中间展示文章信息,两边展示广告;
    2. 为中部文章添加分页器,每页展示5篇
    3. 头像展示 已暴露头像资源,直接拼接路径即可
    

    效果:


    代码:

    分页器代码 app01\utils\split_page.py

    点击查看分页器代码
    class Pagination(object):
        def __init__(self, current_page, all_count, per_page_num=10, pager_count=11):
            """
            封装分页相关数据
            :param current_page: 当前页
            :param all_count:    数据库中的数据总条数
            :param per_page_num: 每页显示的数据条数
            :param pager_count:  最多显示的页码个数
            """
            try:
                current_page = int(current_page)
            except Exception as e:
                current_page = 1
    
            if current_page < 1:
                current_page = 1
    
            self.current_page = current_page
    
            self.all_count = all_count
            self.per_page_num = per_page_num
    
            # 总页码
            all_pager, tmp = divmod(all_count, per_page_num)
            if tmp:
                all_pager += 1
            self.all_pager = all_pager
    
            self.pager_count = pager_count
            self.pager_count_half = int((pager_count - 1) / 2)
    
        @property
        def start(self):
            return (self.current_page - 1) * self.per_page_num
    
        @property
        def end(self):
            return self.current_page * self.per_page_num
    
        def page_html(self):
            # 如果总页码 < 11个:
            if self.all_pager <= self.pager_count:
                pager_start = 1
                pager_end = self.all_pager + 1
            # 总页码  > 11
            else:
                # 当前页如果<=页面上最多显示11/2个页码
                if self.current_page <= self.pager_count_half:
                    pager_start = 1
                    pager_end = self.pager_count + 1
    
                # 当前页大于5
                else:
                    # 页码翻到最后
                    if (self.current_page + self.pager_count_half) > self.all_pager:
                        pager_end = self.all_pager + 1
                        pager_start = self.all_pager - self.pager_count + 1
                    else:
                        pager_start = self.current_page - self.pager_count_half
                        pager_end = self.current_page + self.pager_count_half + 1
    
            page_html_list = []
            # 添加前面的nav和ul标签
            page_html_list.append('''
                        <nav aria-label='Page navigation>'
                        <ul class='pagination'>
                    ''')
            first_page = '<li><a href="?page=%s">首页</a></li>' % (1)
            page_html_list.append(first_page)
    
            if self.current_page <= 1:
                prev_page = '<li class="disabled"><a href="#">上一页</a></li>'
            else:
                prev_page = '<li><a href="?page=%s">上一页</a></li>' % (self.current_page - 1,)
    
            page_html_list.append(prev_page)
    
            for i in range(pager_start, pager_end):
                if i == self.current_page:
                    temp = '<li class="active"><a href="?page=%s">%s</a></li>' % (i, i,)
                else:
                    temp = '<li><a href="?page=%s">%s</a></li>' % (i, i,)
                page_html_list.append(temp)
    
            if self.current_page >= self.all_pager:
                next_page = '<li class="disabled"><a href="#">下一页</a></li>'
            else:
                next_page = '<li><a href="?page=%s">下一页</a></li>' % (self.current_page + 1,)
            page_html_list.append(next_page)
    
            last_page = '<li><a href="?page=%s">尾页</a></li>' % (self.all_pager,)
            page_html_list.append(last_page)
            # 尾部添加标签
            page_html_list.append('''
                                               </nav>
                                               </ul>
                                           ''')
            return ''.join(page_html_list)
    
    
    视图层 app01\views.py
    点击查看代码
    from app01.utils.split_page import Pagination
    def home(request):
        # 分页器
        article_obj_list = models.Article.objects.all()  #获取到所有文章对象用于首页展示
        current_page = request.GET.get('page',1)
        all_count = article_obj_list.count()
    
        # 1. 传值生成对象
        page_obj = Pagination(current_page=current_page,all_count=all_count,per_page_num=5)
        # 2. 直接对数据集进行切片
        page_queryset = article_obj_list[page_obj.start:page_obj.end]
        return render(request,'home.html',locals())
    

    模板层 templates\home.html

    点击查看首页文章展示代码
        <div class="row">
            <!-- 左侧广告 -->
            <div class="col-md-2">
                <div class="jumbotron">
                    <h1>python零基础入学!</h1>
                    <p>人工智能&数据分析实战</p>
                    <p><a class="btn btn-primary btn-lg" href="#" role="button">了解更多</a></p>
                </div>
                <div class="panel panel-danger">
                    <div class="panel-heading">Fun! 知识开放开放麦</div>
                    <div class="panel-body">
                        快来加入我们吧~
                    </div>
                </div>
                <div class="panel panel-warning">
                    <div class="panel-heading">老百金</div>
                    <div class="panel-body">
                        今年过节不收礼
                    </div>
                </div>
            </div>
            <!-- 中间文章展示 -->
            <div class="col-md-8">
                <br>
                {% for article_obj in page_queryset %}
                <h3 style="color: royalblue;font-weight:bold;"><a href="">{{ article_obj.title }}</a></h3>
                <!-- 媒体对象 头像左,内容右 -->
                <div class="media">
                    <div class="media-left media-middle">
                      <a href="#">
                        <!-- 用户头像 -->
                        <img class="media-object" src="/media/{{ article_obj.blog.userinfo.avatar }}" alt="" style=" 60px;">
                      </a>
                    </div>
                    <div class="media-body">
                        <p>{{ article_obj.desc }}</p>
                    </div>
                </div>
                <br>
                <!-- 用户名    文章创建事件   点赞图标 点赞数   评论图标 评论数 -->
                <!-- 点击用户名跳转个人主页 -->
                <a href="/{{ article_obj.blog.userinfo.username }}">{{ article_obj.blog.userinfo.username }}</a> &nbsp;&nbsp;
                {{ article_obj.create_time|date:'Y-m-d' }} &nbsp;&nbsp;
                <span class="glyphicon glyphicon-thumbs-up"></span> &nbsp;{{ article_obj.up_num }} &nbsp;&nbsp;
                <span class="glyphicon glyphicon-comment"></span> &nbsp;{{ article_obj.comment_num }} 
                
                <!-- 分页器代码 -->
                <nav aria-label="Page navigation">
                </nav>
    
                <br><br><hr>
                {% endfor %}
    
                <!-- 分页器代码 -->
                {{ page_obj.page_html|safe }}
            </div>
            <!-- 右侧广告 -->
            <div class="col-md-2">
                <div class="panel panel-primary">
                    <div class="panel-heading">ANTA</div>
                    <div class="panel-body">
                        安踏 永不止步
                    </div>
                </div>
                <div class="panel panel-success">
                    <div class="panel-heading">鸿星尔克 </div>
                    <div class="panel-body">
                        TO BE NUMBER 1
                    </div>
                </div>
                <div class="jumbotron">
                    <h1>2核4G 298 3年!</h1>
                    <p>广厦云服务</p>
                    <p><a class="btn btn-primary btn-lg" href="#" role="button">我感兴趣</a></p>
                </div>
            </div>
            </div>
        </div>
    
    

    更换头像

    1. 入口: 登陆后点击更多信息中的更换头像,弹出模态框
    2. 点击原头像图像弹出文件选择窗口,选择好新图片后展示为新的头像
    3. 点击确认数据库更新头像,前端重载页面
    

    效果:

    代码:

    路由层 BBS\urls.py

    点击查看代码
        #更换头像
        url(r'^change_avatar/',views.change_avatar,name='changeAvatar'),
    

    视图层 app01\views.py

    点击查看代码
    @login_required
    def change_avatar(request):
        back_dic = {'code':1000}
        if request.method == 'POST':
            new_avatar = request.FILES.get('new_avatar')
            request.user.avatar = new_avatar
            request.user.save()
        return JsonResponse(back_dic)
    

    模板层 templates\home.html

    点击查看代码
                                <!-- 入口: 添加 data-toggle="modal" data-target=".vatar-modal-lg" 点击弹出更换头像的模态框 -->
                                <li><a href="" data-toggle="modal" data-target=".vatar-modal-lg">更换头像</a></li>
    
        <!-- 修改头像模态框 -->
        <div class="modal fade vatar-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
            <div class="modal-dialog modal-lg" role="document">
              <div class="modal-content">
                  <h2 class="text-center">更换头像</h2>
                  <div class="row">
                      <div class="col-md-8 col-md-offset-2">
                        <label for="id_avatar">
                            头像:
                            <img src="/media/{{ request.user.avatar }}" alt="" style=" 150px;" id="img_avatar">
                            <!-- <img src="{% static 'img/default.png' %}" alt="" id='myimg' style=" 100px;margin-left: 10px;"> -->
                        </label>
                        <input type="file" id="id_avatar" style="display:none">
                        <div class="modal-footer">
                            <span class="pull-left" id="prompt" style="color: red;"></span>
                            <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                            <button type="button" class="btn btn-primary" id="change_vatar_btn">确认</button>
                        </div>
                      </div>
                  </div>
              </div>
            </div>
        </div>
    
        <script>
            // 修改头像处上传后更新展示
            $('#id_avatar').change(function(){
                // 文件阅读器对象
                // 1. 生成一个文件阅读器
                let myFileReaderObj = new FileReader();
                // 2. 获取用户上传的头像文件
                let fileObj = $(this)[0].files[0];
                // 3. 将文件对象交给阅读器处理   
                myFileReaderObj.readAsDataURL(fileObj)  // 异步 IO操作 所以这里文件还没读完就会执行下一行代码
                // 4. 利用阅读器将文件展示到前端页面   
                myFileReaderObj.onload = function(){  // onload 等待myFileReaderObj对象的任务都执行完成才执行
                    $('#img_avatar').attr('src',myFileReaderObj.result)
                }
            })
            // 修改头像提交
            $('#change_vatar_btn').click(function(){
                let formDataObj = new FormData();
                formDataObj.append('new_avatar',$('#id_avatar')[0].files[0])
                $.ajax({
                    url: "{% url 'changeAvatar' %}",
                    method: 'post',
                    data: formDataObj,
                    contentType: false,
                    processData: false,
                    success: function(args){
                        if (args.code==1000){window.location.reload()}
                        else{window.location.reload()}   
                    },
                })
            })
        </script>
    

    个人站点展示

    1. 个人站点:  http://127.0.0.1/用户名/
    
    
    2. 用户名不存在,返回404页面   
       直接拷贝的博客园404页面代码
       图片不能展示 --> 图片防盗链
       
    3. 用户存在:
       顶部一个导航条
       下面3-9布局,右侧默认展示该用户的所有文章
       
    3. 左侧侧边栏展示文章分类、标签、创建时间 年-月,点击不同分组右侧展示对应的文章
       http://127.0.0.1:8000/yxf/category/4(该分类的主键值)   分类链接
       http://127.0.0.1:8000/yxf/tag/6(该标签的主键值)        标签链接
       http://127.0.0.1:8000/yxf/archive/2022-05             create_time分组链接
       
    4. 创建时间按 年-月 分组,并统计每组文章数
        models.Article.objects.filter(blog=blog_obj) # 对象集
        .annotate(month=TruncMonth('create_time'))   # 将create_time字段截取到月(2022-3),并将此字段month添加到查询列表中
        .values('month').annotate(c=Count('pk'))     # 按month字段分组,并且每组根据主键计数
        .values_list('month','c')                    # 返回month与c字段
    
        返回: 虽然month截取到月,但是这里返回还是有day,默认都为1号
        [(datetime.date(2022, 5, 1), 1), (datetime.date(2022, 3, 1), 2),...]
    
        如果报错,可尝试修改配置文件中的时区:
    
        # TIME_ZONE = 'UTC'
        TIME_ZONE = 'Asia/Shanghai'
        # USE_TZ = True
        USE_TZ = False
    
    5. 个人主页引入自定义样式
       每个用户都可以有自己的站点样式
    
       1)在site.html中加载自定义样式文件
       <link rel="stylesheet" href="/media/css/{{ blog_obj.site_theme }}">
    
       2)创建样式文件 media\css\huandada.css 这里的文件名要与blog表的site_theme的文件名一致
    

    效果:
    个人主页:

    文章分类展示:

    文章标签展示:

    日期分组展示:

    自定义样式展示: a标签变绿

    代码:

    路由层 BBS\urls.py

    点击查看代码
        # 个人站点
        url(r'^(?P<username>\w+)/$',views.site,name='site'),
        # 个人站点侧边栏筛选功能 (文章category/tag/create_time分组展示)
        url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/',views.site)
    

    视图层 app01\views.py

    点击查看代码
    from django.db.models.functions import TruncMonth
    from django.db.models import Count
    
    # 个人站点
    def site(request,username,**kwargs):
        '''
        :param kwargs: 如果此参数有值,就是个人主页侧边栏的分组展示,也就意味着 article_list 要做额外筛选
        '''
    
        user_obj = models.UserInfo.objects.filter(username=username)
        # 如果该用户不存在返回404页面
        if not user_obj:
            return render(request,'404.html')
        # 用户存在就展示相关的内容
        else:
            blog_obj = user_obj[0].blog
            # 该用户的文章列表,kwargs没值,就展示所有文章
            article_list = blog_obj.article_set.all()
    
            # 如果kwargs有值,就是个人主页侧边栏category/tag/create_time分组展示
            if kwargs:
                # print(kwargs)  # {'condition': 'category', 'param': '4'}
                condition = kwargs.get('condition')
                param = kwargs.get('param')
    
                # 文章分类查询
                if condition == 'category':
                    article_list = article_list.filter(category__pk=param)
                # 文章标签查询
                if condition == 'tag':
                    article_list = article_list.filter(tags__pk=param)
                # 创建时间按 年-月 查询
                if condition == 'archive':
                    year,month = param.split('-')  #2022-3 => [2022,3]
                    article_list = article_list.filter(create_time__year=year,create_time__month=month)
    
            # 查询该用户的文章分类及对应的每一类对于的文章数
            category_list = models.Category.objects.filter(blog=blog_obj).annotate(count_num=Count('article__pk')).values_list('name','count_num','pk')
                # <QuerySet [('猫粮', 2), ('猫咪生活', 2), ('喵品种', 3)]>
            # 查询该用户的标签及每种标签对于的文章数
            tag_list = models.Tag.objects.filter(blog=blog_obj).annotate(count_num=Count('article__pk')).values_list('name','count_num','pk')
                # <QuerySet [('yxf标签一', 1), ('yxf标签二', 2), ('yxf标签三', 1)]>
            # 将该用户文章创建日期按 年-月 分组并统计文章数
            date_list = models.Article.objects.filter(blog=blog_obj).annotate(month=TruncMonth('create_time')).values('month').annotate(c=Count('pk')).values_list('month','c')
            '''
            # 日期字段 按 年-月 分组
            models.Article.objects.filter(blog=blog_obj) # 对象集
            .annotate(month=TruncMonth('create_time'))   # 将create_time字段截取到月(2022-3),并将此字段month添加到查询列表中
            .values('month').annotate(c=Count('pk'))     # 按month字段分组,并且每组根据主键计数
            .values_list('month','c')                    # 返回month与c字段
    
            返回: 虽然month截取到月,但是这里返回还是有day,默认都为1号
              [(datetime.date(2022, 5, 1), 1), (datetime.date(2022, 3, 1), 2),...]
            ''' 
                # 对时间对象进行反向排序
            date_list = sorted(date_list,key=lambda i:i[0],reverse=True)
    
            return render(request,'site.html',locals())
    

    模板层
    templates\404.html

    点击查看代码
    <!-- 直接cp的博客园404页面代码 -->
    <html>
    <head>
        <meta charset='utf-8'>
        <link rel="icon" href="//common.cnblogs.com/favicon.ico" type="image/x-icon" />
        <title>404 页面不存在 - 博客园</title>
        <style type='text/css'>
            body {
                margin: 8% auto 0;
                max- 400px;
                min-height: 200px;
                padding: 10px;
                font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
                font-size: 14px;
                padding-right: 200px;
                position: relative;
            }
            p { color: #555;margin: 15px 0px; }
            img { border: 0px; }
            .d { color: #404040; }
            .robot img { max- 192px; }
            .robot { position: absolute; top: 0; right: 0; }
        </style>
    </head>
    <body>
        <p style="margin-left: 5px;"><a href="https://www.cnblogs.com/"><img src="//common.cnblogs.com/logo.svg" style="height:45px" alt="cnblogs"></a></p>
        <div style="margin-top:20px">
            <p><b>404.</b> 抱歉,您访问的资源不存在。</p>
            <p class="d">可能是网址有误,或者对应的内容被删除,或者处于私有状态。</p>
            <p style="color:#777;">代码改变世界,联系邮箱 contact@cnblogs.com</p>
        </div>
        <div class="robot"><a href="//www.cnblogs.com/cmt/articles/13940458.html"><img src="//common.cnblogs.com/images/404-robot.png" alt="404 robot" /></a></div>
        <script async src="https://www.googletagmanager.com/gtag/js?id=G-4CQQXWHK3C"></script>
        <script>
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
    
          gtag('config', 'G-4CQQXWHK3C');
        </script>
    </body>
    </html>
    

    templates\site.html

    点击查看代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        {% load static %}
        <link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
        <script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
        <script src="{% static '/js/mysetup.js' %}"></script>
        <title>BBS</title>
        <!-- 引入自定义样式 -->
        <link rel="stylesheet" href="/media/css/{{ blog_obj.site_theme }}">
    </head>
    <body>
        <!-- 导航条 直接cp的home.html的导航条,改了一个地方。可根据需求自行更改 -->
        <nav class="navbar navbar-inverse">
            <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="/{{ username }}">{{ blog_obj.site_name }}</a>
                </div>
            
                <!-- Collect the nav links, forms, and other content for toggling -->
                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                    <li class="active"><a href="#">博客 <span class="sr-only">(current)</span></a></li>
                    <li><a href="#">文章</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="#">Action</a></li>
                        <li><a href="#">Another action</a></li>
                        <li><a href="#">Something else here</a></li>
                        <li role="separator" class="divider"></li>
                        <li><a href="#">Separated link</a></li>
                        <li role="separator" class="divider"></li>
                        <li><a href="#">One more separated link</a></li>
                    </ul>
                    </li>
                </ul>
                <form class="navbar-form navbar-left">
                    <div class="form-group">
                    <input type="text" class="form-control" placeholder="Search">
                    </div>
                    <button type="submit" class="btn btn-default">Submit</button>
                </form>
                <ul class="nav navbar-nav navbar-right">
                    {% if request.user.is_authenticated %}
                            <li><a href="">{{ request.user.username }}</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="" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
                                <li><a href="" data-toggle="modal" data-target=".vatar-modal-lg">更换头像</a></li>
                                <li><a href="#">后台管理</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="{% url 'logout' %}">退出登录</a></li>
                                </ul>
                            </li>
                    {% else %}  
                        <li><a href="/login/">登录</a></li>
                        <li><a href="/register/">注册</a></li>    
                    {% endif %}
                    
                    </ul>
                    </li>
                </ul>
                </div><!-- /.navbar-collapse -->
            </div><!-- /.container-fluid -->
        </nav>
    
        <div class="container-fluid">
            <div class="row">
                <!-- 左侧 侧边栏 展示 文章分类/标签/创建时间 的分组 及对应的文章数 -->
                <div class="col-md-3">
                    <div class="panel panel-success">
                        <div class="panel-heading">文章分类</div>
                        <div class="panel-body">
                            {% for category in category_list %}
                                <p><a href="/{{ username }}/category/{{ category.2 }}">{{ category.0 }} ({{ category.1 }})</a></p>
                            {% endfor %}
                        </div>
                    </div>
                    <div class="panel panel-success">
                        <div class="panel-heading">文章标签</div>
                        <div class="panel-body">
                            {% for tag in tag_list %}
                            <p><a href="/{{ username }}/tag/{{ tag.2 }}">{{ tag.0 }} ({{ tag.1 }})</a></p>
                            {% endfor %}
                        </div>
                    </div>
                    <div class="panel panel-success">
                        <div class="panel-heading">日期归档</div>
                        <div class="panel-body">
                            {% for create_time in date_list %}
                            <p><a href="/{{ username }}/archive/{{ create_time.0|date:'Y-m' }}">{{ create_time.0|date:'Y-m' }} ({{ create_time.1 }})</a></p>
                            {% endfor %}
                        </div>
                    </div>
                </div>
                <!-- 右侧 展示 article_list文章中的文章对象 -->
                <div class="col-md-9">
                    <br>
                    {% for article_obj in article_list %}
                    <h3 style="font-weight:bold;"><a href="">{{ article_obj.title }}</a></h3>
                    <!-- 媒体对象 头像左,内容右 -->
                    <div class="media">
                        <div class="media-left media-middle">
                            <a href="#">
                            <img class="media-object" src="/media/{{ blog_obj.userinfo.avatar }}" alt="" style=" 60px;">
                            </a>
                        </div>
                        <div class="media-body">
                            <p>{{ article_obj.desc }}</p>
                        </div>
                    </div>
                    <!-- posted @  文章创建时间   点赞图标 点赞数   评论图标 评论数 编辑-->
                    <div class="pull-right" style="margin-right: 30px">
                        <span>posted @ {{ article_obj.create_time|date:'Y-m-d' }} &nbsp;&nbsp;</span>
                        <span class="glyphicon glyphicon-thumbs-up"></span> &nbsp;{{ article_obj.up_num }} &nbsp;&nbsp;
                        <span class="glyphicon glyphicon-comment"></span> &nbsp;{{ article_obj.comment_num }} &nbsp;&nbsp;
                        <span><a href="">编辑</a></span>
                    </div>
                    <br><br><hr>
                    {% endfor %}
        
                </div>
            </div>
        </div>
    </body>
    </html>
    

    自定义样式文件 media\css\huandada.css(可以没有)

    点击查看代码
    /* a标签颜色 */
    a {
        color: green;
    }
    

    图片防盗链

    直接拷贝的博客园404页面代码,本项目使用,图片展示不全,因为博客园对该图片链接设置了图片防盗

    博客园的404页面

    拷贝404代码到我的项目后的页面

    # 如何避免别的网站通过本网站的url访问本网站的资源 ?
    
    # 简单防盗
    当请求来的 时候,先看当前请求是从哪个网站来的,
    如果是本网站来的,则正常访问;
    是其他网站来的,则拒绝访问。
    
    请求头中的Referer是专门记录请求来源于哪个网站的
        Referer: http://127.0.0.1:8000/
        referer: https://www.cnblogs.com/
    
    # 如何绕过这种防盗措施
    1. 修改请求头中的 Referer地址
    2. 直接爬虫把对方的资源下载到我们本地
    

    模板继承 与 inclusion_tag

    侧边栏需要后端传输数据才能渲染,并且多个页面都有侧边栏的,那么:

    1. 在需要侧边栏的地方直接拷贝侧边栏的代码(不推荐)
    2. 将侧边栏制作成inclusion_tag,哪里需要就在哪里加载

    侧边栏做成inclusion_tag形式:

    定义inclusion_tag app01\templatetags\mytags.py

    直接 剪切 site视图函数中侧边栏的代码

    点击查看代码
    from django import template
    from app01 import models
    from django.db.models.functions import TruncMonth
    from django.db.models import Count
    
    register=template.Library()
    @register.inclusion_tag('left_menu.html')
    def left_menu(username):
        user_obj = models.UserInfo.objects.filter(username=username)[0]
        blog_obj = user_obj.blog
        # 查询该用户的文章分类及对应的每一类对于的文章数
        category_list = models.Category.objects.filter(blog=blog_obj).annotate(count_num=Count('article__pk')).values_list('name','count_num','pk')
            # <QuerySet [('猫粮', 2), ('猫咪生活', 2), ('喵品种', 3)]>
        # 查询该用户的标签及每种标签对于的文章数
        tag_list = models.Tag.objects.filter(blog=blog_obj).annotate(count_num=Count('article__pk')).values_list('name','count_num','pk')
            # <QuerySet [('yxf标签一', 1), ('yxf标签二', 2), ('yxf标签三', 1)]>
        # 将该用户文章创建日期按 年-月 分组并统计文章数
        date_list = models.Article.objects.filter(blog=blog_obj).annotate(month=TruncMonth('create_time')).values('month').annotate(c=Count('pk')).values_list('month','c')
        '''
        # 日期字段 按 年-月 分组
        models.Article.objects.filter(blog=blog_obj) # 对象集
        .annotate(month=TruncMonth('create_time'))   # 将create_time字段截取到月(2022-3),并将此字段month添加到查询列表中
        .values('month').annotate(c=Count('pk'))     # 按month字段分组,并且每组根据主键计数
        .values_list('month','c')                    # 返回month与c字段
    
        返回: 虽然month截取到月,但是这里返回还是有day,默认都为1号
            [(datetime.date(2022, 5, 1), 1), (datetime.date(2022, 3, 1), 2),...]
        ''' 
            # 对时间对象进行反向排序
        date_list = sorted(date_list,key=lambda i:i[0],reverse=True) 
        return locals()
    

    个人站点以及后续的文章详情页页面布局一致,可以直接改为模板继承的模式

    模板页面 base.html (直接拷贝site.html稍作修改)

    点击查看代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        {% load static %}
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script>
        <link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
        <script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
        <script src="{% static '/js/mysetup.js' %}"></script>
        <title>BBS</title>
        <!-- 引入自定义样式 -->
        <link rel="stylesheet" href="/media/css/{{ blog_obj.site_theme }}">
        {% block css_part %}
          
        {% endblock css_part %}
    </head>
    <body>
        <!-- 导航条 直接cp的home.html的导航条,改了一个地方。可根据需求自行更改 -->
        <nav class="navbar navbar-inverse">
            <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="/{{ username }}">{{ blog_obj.site_name }}</a>
                </div>
            
                <!-- Collect the nav links, forms, and other content for toggling -->
                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                    <li class="active"><a href="#">博客 <span class="sr-only">(current)</span></a></li>
                    <li><a href="#">文章</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="#">Action</a></li>
                        <li><a href="#">Another action</a></li>
                        <li><a href="#">Something else here</a></li>
                        <li role="separator" class="divider"></li>
                        <li><a href="#">Separated link</a></li>
                        <li role="separator" class="divider"></li>
                        <li><a href="#">One more separated link</a></li>
                    </ul>
                    </li>
                </ul>
                <form class="navbar-form navbar-left">
                    <div class="form-group">
                    <input type="text" class="form-control" placeholder="Search">
                    </div>
                    <button type="submit" class="btn btn-default">Submit</button>
                </form>
                <ul class="nav navbar-nav navbar-right">
                    {% if request.user.is_authenticated %}
                            <li><a href="">{{ request.user.username }}</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="" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
                                <li><a href="" data-toggle="modal" data-target=".vatar-modal-lg">更换头像</a></li>
                                <li><a href="#">后台管理</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="{% url 'logout' %}">退出登录</a></li>
                                </ul>
                            </li>
                    {% else %}  
                        <li><a href="/login/">登录</a></li>
                        <li><a href="/register/">注册</a></li>    
                    {% endif %}
                    
                    </ul>
                    </li>
                </ul>
                </div><!-- /.navbar-collapse -->
            </div><!-- /.container-fluid -->
        </nav>
    
        <div class="container-fluid">
            <div class="row">
                <!-- 左侧 侧边栏 展示 文章分类/标签/创建时间 的分组 及对应的文章数 -->
                <div class="col-md-3">
                    <!-- 引用inclusion_tag left_menu 侧边栏 -->
                    {% load mytags %}
                    {% left_menu username %}
                </div>
                <!-- 右侧 展示 article_list文章中的文章对象 -->
                <div class="col-md-9">
                    {% block content %}
                      
                    {% endblock content %}    
                </div>
            </div>
        </div>
    
        {% block js_part %}
          
        {% endblock js_part %}
    </body>
    </html>
    

    个人站点的页面

    点击查看代码
    {% extends "base.html" %}
    
    {% block content %}
    <br>
    {% for article_obj in article_list %}
    <h3 style="font-weight:bold;"><a href="">{{ article_obj.title }}</a></h3>
    <!-- 媒体对象 头像左,内容右 -->
    <div class="media">
        <div class="media-left media-middle">
            <a href="#">
            <img class="media-object" src="/media/{{ blog_obj.userinfo.avatar }}" alt="" style=" 60px;">
            </a>
        </div>
        <div class="media-body">
            <p>{{ article_obj.desc }}</p>
        </div>
    </div>
    <!-- posted @  文章创建时间   点赞图标 点赞数   评论图标 评论数 编辑-->
    <div class="pull-right" style="margin-right: 30px">
        <span>posted @ {{ article_obj.create_time|date:'Y-m-d' }} &nbsp;&nbsp;</span>
        <span class="glyphicon glyphicon-thumbs-up"></span> &nbsp;{{ article_obj.up_num }} &nbsp;&nbsp;
        <span class="glyphicon glyphicon-comment"></span> &nbsp;{{ article_obj.comment_num }} &nbsp;&nbsp;
        <span><a href="">编辑</a></span>
    </div>
    <br><br><hr>
    {% endfor %}  
    {% endblock content %}
    

    文章详情页

    1. 单篇文章url /用户名/article/文章id
    
    2. 文章详情页继承模板页面
    
    3. 文章内容填充,拷贝博客园 $('#cnblogs_post_body')
    

    效果:

    先在admin后台管理页面添加html格式的文章内容 (直接从博客园cp)

    代码:

    路由层 BBS\urls.py

    点击查看代码
        # 文章详情页
        url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/$',views.article_detail),
    

    视图层 app01\views.py

    点击查看代码
    def article_detail(request,username,article_id):
        user_obj = models.UserInfo.objects.filter(username=username)
        article_obj = models.Article.objects.filter(pk=article_id,blog__userinfo__username=username )
        if not article_obj:
            return render(request,'404.html')
        article_obj = article_obj[0]
        blog_obj = article_obj.blog
    
        return render(request,'article_detail.html',locals())
    

    模板层 templates\article_detail.html

    点击查看代码
    {% extends "base.html" %}
    
    {% block content %}
        <h1 style="font-weight: bold;">{{ article_obj.title }}</h1>
        <br>
        {{ article_obj.content|safe }}
    {% endblock content %}
    

    文章点赞点踩

    1. 点赞点踩url  up_or_down/
    2. 直接cp博客园点赞点踩的代码以及对应的每一个标签的样式
    3. cp 博客园点踩点赞的html代码与css样式
        将图片下载到本地(图片防盗链)
    
    4. 如何区分用户点赞还是点踩
       1) 给点赞的标签绑定id="digg_count"
       2) let is_up = $(this).hasClass('diggit');
         返回true 或 false
    
    5. 点赞点踩内部逻辑
        1)登陆后才能点
        2)不能给自己点
        3)点过了赞/踩 就不能再点
        4)点击之后,点赞/踩数展示+1
    

    效果:

    代码:
    路由层 BBS\urls.py

    点击查看代码
        # 文章点赞点踩
        url(r'^up_or_down/',views.up_or_down),
    

    视图层 app01\views.py

    点击查看代码
    import json
    from django.db.models import F
    def up_or_down(request):
        back_dic={'code':1000}
        print('in updown')
        if request.method == 'POST':
            # 判断用户是否登录
            if request.user.is_authenticated():
                is_up = request.POST.get('is_up')
                article_id = request.POST.get('article_id')
                # 将is_up 从str转为bool
                is_up = json.loads(is_up)
    
                article_obj_query = models.Article.objects.filter(pk=article_id)
                article_obj = article_obj_query.first()
                # 判断文章是否存在
                if article_obj:
                    article_user_obj = article_obj.blog.userinfo
                    # 如果点赞/踩 人就是文章作者自己
                    if not request.user == article_user_obj:
                        clicked = models.UpAndDown.objects.filter(article=article_obj,user=request.user).first()
                        # 如果该用户已经对该文章点过赞/踩了
                        if not clicked:
                            # 写入到点赞点踩表
                            models.UpAndDown.objects.create(article=article_obj,user=request.user,is_up=is_up)
                            if is_up:
                                back_dic['msg']='点赞成功'
                                article_obj_query.update(up_num=F('up_num')+1)
                            else:
                                back_dic['msg']='点踩成功'
                                article_obj_query.update(down_num=F('down_num')+1)
                        else:
                            if clicked.is_up:
                                back_dic['code']='1001'
                                back_dic['msg']='您已点过赞了'
                            else:
                                back_dic['code']='1001'
                                back_dic['msg']='您已点过踩了'
                    else:
                        if is_up:
                            back_dic['code']='1002'
                            back_dic['msg']='您不能给自己点赞哦'    
                        else:
                            back_dic['code']='1002'
                            back_dic['msg']='您不能给自己点踩哦'              
                else: 
                    return render(request,'404.html')
            else:
                back_dic['code']='1003'
                back_dic['msg']='请先<a href="/login/" style="color:red">登录</a>'
            return JsonResponse(back_dic)
    

    模板层 templates\article_detail.html

    点击查看代码
    {% extends "base.html" %}
    {% block css_part %}
    <!-- 博客园cp的点赞点踩样式开始 -->
    <style>
        #div_digg {
        float: right;
        margin-bottom: 10px;
        margin-right: 30px;
        font-size: 12px;
         125px;
        text-align: center;
        margin-top: 10px;
        }
        .diggit {
            float: left;
             46px;
            height: 52px;
            background: url(/static/img/upup.gif) no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }
        .buryit {
            float: right;
            margin-left: 20px;
             46px;
            height: 52px;
            background: url(/static/img/downdown.gif) no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }
        .clear {
            clear: both;
        }
        .diggword {
            margin-top: 5px;
            margin-left: 0;
            font-size: 12px;
            color: #808080;
        }
    </style>
    <!-- 博客园cp的点赞点踩样式结束 -->
    {% endblock css_part %}
    {% block content %}
        <!-- 文章展示 -->
        <h1 id='title' style="font-weight: bold;" article_id="{{ article_obj.pk }}">{{ article_obj.title }}</h1>
        <br>
        {{ article_obj.content|safe }}
    
        <!-- 点赞点踩代码开始 -->
        <div id="div_digg">
            <div class="diggit upAndDown">
                <span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
            </div>
            <div class="buryit upAndDown">
                <span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
            </div>
            <div class="clear"></div>
    
            <span style="color: red;" id="up_down_comment"></span>
        </div>
        <!-- 点赞点踩代码结束 -->
    {% endblock content %}
    
    {% block js_part %}
        <script>
            // 点赞点踩请求提交与返回结果渲染
            $('.upAndDown').click(function(){
                let is_up = $(this).hasClass('diggit');
                let article_id = $('#title').attr('article_id');
                let clicked = $(this)
                $.ajax({
                    url: '/up_or_down/',
                    method: 'post',
                    data: {'is_up':is_up,'article_id':article_id,'csrfmiddlewaretoken':'{{ csrf_token }}'},
                    success: function(args){
                        if (args.code==1000){
                            $('#up_down_comment').text(args.msg);
                            let num = clicked.children().text()
                            clicked.children().text(Number(num)+1)
                            }
    
                        else{
                            // 这里用html不用text是因为msg中有个登录的a标签
                            $('#up_down_comment').html(args.msg)
                            }
                    }
                })
            })
        </script>
    {% endblock js_part %}
    

    文章评论

    1. 使用ajax提交评论内容
    2. 分为两部分:
      1) 评论框提交评论
      2) 展示评论
         - 已评论内容 展示,每篇文章展示其自己的评论
         - 刚评论内容 临时渲染
    3. 评论分为两种:
       1) 根评论: 点击评论框直接评论
       2) 子评论: 点击某一条评论的回复/引用,自动聚焦到评论框并@父评论用户名或信息
    4. 限制:
       1) 要先登录
       2)评论内容不能为空
    

    效果:

    代码:
    路由层 BBS\urls.py

    点击查看代码
        # 评论
        url(r'^comment/',views.comment),
    

    视图层 app01\views.py

    点击查看代码
    def article_detail(request,username,article_id):
        user_obj = models.UserInfo.objects.filter(username=username)
        article_obj = models.Article.objects.filter(pk=article_id,blog__userinfo__username=username).first()
        if not article_obj:
            return render(request,'404.html')
        blog_obj = article_obj.blog
        # 拿到该篇文章的评论数据
        comment_list = models.Comment.objects.filter(article=article_obj).all()
        return render(request,'article_detail.html',locals())
    
    # 评论功能
    from django.db import transaction
    def comment(request):
        back_dic={'code':1000}
        if request.is_ajax() and request.method == 'POST':
            comment = request.POST.get('comment')
            article_id = request.POST.get('article_id')
            parent_id = request.POST.get('parent')
            # 判断用户是否登录
            if request.user.is_authenticated():
                # 判断评论是否为空
                if comment:
                    article_obj=models.Article.objects.filter(pk=article_id).first()
    
                    with transaction.atomic():
                        models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num')+1)
                        models.Comment.objects.create(user=request.user,article=article_obj,content=comment,parent_id=parent_id)
                else:
                    back_dic['code']=1001
                    back_dic['msg']='评论内容不能为空'
            else:
                back_dic['code']=1002
                back_dic['msg']='请先<a href="/login/" style="color:red">登录</a>  <a href="/register/" style="color:red">注册</a>'
        return JsonResponse(back_dic)
    

    模板层 templates\article_detail.html

    点击查看代码
    {% extends "base.html" %}
    {% block css_part %}
    <!-- 博客园cp的点赞点踩样式开始 -->
    <style>
        #div_digg {
        float: right;
        margin-bottom: 10px;
        margin-right: 30px;
        font-size: 12px;
         125px;
        text-align: center;
        margin-top: 10px;
        }
        .diggit {
            float: left;
             46px;
            height: 52px;
            background: url(/static/img/upup.gif) no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }
        .buryit {
            float: right;
            margin-left: 20px;
             46px;
            height: 52px;
            background: url(/static/img/downdown.gif) no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }
        .clear {
            clear: both;
        }
        .diggword {
            margin-top: 5px;
            margin-left: 0;
            font-size: 12px;
            color: #808080;
        }
    </style>
    <!-- 博客园cp的点赞点踩样式结束 -->
    {% endblock css_part %}
    {% block content %}
        <!-- 文章展示 -->
        <h1 id='title' style="font-weight: bold;" article_id="{{ article_obj.pk }}">{{ article_obj.title }}</h1>
        <br>
        {{ article_obj.content|safe }}
    
        <!-- 点赞点踩代码开始 -->
        <div id="div_digg" class="clearfix">
            <div class="diggit upAndDown">
                <span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
            </div>
            <div class="buryit upAndDown">
                <span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
            </div>
            <div class="clear"></div>
    
            <span style="color: red;" id="up_down_comment"></span>
        </div>
        <br><br><br>
        <!-- 点赞点踩代码结束 -->
        
        <!-- 评论功能开始 -->
        
            <!-- 评论信息展示 -->
        <div class="row">
            <div class="col-md-8">
                <div>
                    {% for comment_obj in comment_list %}
                    <hr>
                    <div>#{{ forloop.counter }}楼 {{ comment_obj.comment_time|date:'Y-m-d H:i:s'}} <a href="/{{ comment_obj.user.username }}">{{ comment_obj.user.username }}</a></div>
                    <br>
                    {% if comment_obj.parent %}
                    <div>@{{ comment_obj.parent.user.username }}</div>
                    {% endif %}
                    <div>{{ comment_obj.content }}</div>
                    <div class="pull-right" username="{{ comment_obj.user.username }}" comment_id="{{ comment_obj.pk }}"> 
                        <a class="comment_replay replay">回复</a>&nbsp;
                        <a class="comment_replay quote">引用</a>
                    </div>
                    {% endfor %}
                    <hr>
                    <!-- 临时渲染评论 -->
                    <div id="new_comment"></div>
                </div>
            </div>
        </div>
    
            <!-- 评论框 -->
        <div>
            <h2>发表评论</h2>
            <div>
                <textarea id="comment_box" cols="70" rows="10"></textarea>
            </div>
            
            <button type="button" id="commit_comment"  class="btn btn-default"> 提交评论 </button>&nbsp;&nbsp;
            <button type="button" id="cancel_comment"  class="btn btn-default"> 取消 </button>&nbsp;&nbsp;
            <span style="color: red;" id="comment_msg"></span>
        </div>
        <!-- 评论功能结束 -->
    
    {% endblock content %}
    
    {% block js_part %}
    <script>
    </script>
        <script>
            // 点赞点踩请求提交与返回结果渲染
            $('.upAndDown').click(function(){
                let is_up = $(this).hasClass('diggit');
                let article_id = $('#title').attr('article_id');
                let clicked = $(this)
                $.ajax({
                    url: '/up_or_down/',
                    method: 'post',
                    data: {'is_up':is_up,'article_id':article_id,'csrfmiddlewaretoken':'{{ csrf_token }}'},
                    success: function(args){
                        if (args.code==1000){
                            $('#up_down_comment').text(args.msg);
                            let num = clicked.children().text()
                            clicked.children().text(Number(num)+1)
                            }
    
                        else{
                            // 这里用html不用text是因为msg中有个登录的a标签
                            $('#up_down_comment').html(args.msg)
                            }
                    }
                })
            })
    
            // ajax提交评论
            let parent_id = ''
            let paren_username = ''
            $('#commit_comment').click(function(){
               let comment = $('#comment_box').val();
               if (parent_id){
                   index_num = comment.indexOf('\n')
                   comment = comment.slice(index_num+1)
               }
               let article_id = $('#title').attr('article_id');
                   $.ajax({
                       url: '/comment/',
                       method: 'post',
                       data: {'comment':comment,'article_id':article_id,'parent':parent_id},
                       success: function(args){
                        //    1000为评论成功
                           if (args.code==1000){
                            // 临时渲染评论
                                // 子评论会多一个 @父评论用户名
                                if (parent_id){
                                    new_comment = `<div><div><span class="glyphicon glyphicon-comment"></span>&nbsp;&nbsp;<span>{{ request.user.username }}</span></div><div>@${parent_username}</div><div>${comment}</div><hr></div>`
                                    $('#new_comment').append(new_comment)
                                }
                                else{
                                new_comment = `<div><div><span class="glyphicon glyphicon-comment"></span>&nbsp;&nbsp;<span>{{ request.user.username }}</span><br><br></div><div>${comment}</div><hr></div>`
                                $('#new_comment').append(new_comment)
                                }
                                $('#comment_box').val('')
                            }
                        //   评论失败,展示提示信息 
                           else{$('#comment_msg').html(args.msg)}
                           parent_id = ''
                       }
                   })
               })
    
            // 评论框获取到焦点,清空提示信息
            $('#comment_box').focus(function(){
                $('#comment_msg').text('')
            })
    
            // 评论框的取消按钮,点击后取消评论(主要用于取消子评论)
            $('#cancel_comment').click(function(){
                $('#comment_box').val('')
                parent_id = ''
            })
            // 点击评论的 回复 自动获取评论框焦点,并自动输入了@父评论用户名\n
            // 点击评论的 引用 自动获取评论框焦点,并自动输入了@父评论用户名\n父评论\n------\n
            $('.comment_replay').click(function(){
                parent_username = $(this).parent().attr('username')
                parent_id = $(this).parent().attr('comment_id')
                $('#comment_box').focus()
                // 回复标签右replay类,所以点击回复is_replay为true,点击引用为false
                is_replay = $(this).hasClass('replay')
                
                if (is_replay){$('#comment_box').val(`@${parent_username}\n`)}
                else {
                   let content = $(this).parent().prev().text()
                   $('#comment_box').val(`@${parent_username}\n${content}\n------\n`)
                }   
            })
        </script>
    
       
    {% endblock js_part %}
    

    **富文本编辑器

    后续后台管理中会用到富文本编辑器,这里先介绍怎么在我们的项目中集成此编辑器
    编辑器官网: http://kindeditor.net/demo.php

    1. 首先下载此编辑器,放到项目的静态资源下

    2. html页面加载编辑器资源

    <script charset="utf-8" src="{% static '/others/editor/kindeditor.js' %} "></script>
    <textarea name="content" id="edit" cols="200" rows="10"></textarea>
    <script>
            KindEditor.ready(function(K) {
                    window.editor = K.create('#edit');
            });
    </script>
    

    1. 相关的常用配置
        KindEditor.ready(function(K) {
                window.editor = K.create('#edit',{
                     '100%',         // 编辑框宽度设置       
                    height: '600px',       // 编辑框高度设置 
                    resizeType: 1,         // resizeType为1,表示也没事编辑框高度可调,宽度不可变
    
                    // 以下都是上传图片/文件相关配置
                    uploadJson : '/upload_img/',  // 上传文件的url
                    extraFileUploadParams : {
                        'csrfmiddlewaretoken':'{{ csrf_token }}',  // 额外携带的参数
                    }
                }
            )
        });
    

    后台管理

    url:
    
    /backend/ 后台管理页面
    /upload_img/  富文本编辑器上传图片
    /change/article/14/  修改某个文章
    /add/article/ 添加新文章
    /delete/article/6  删除某个文章
    /backend/recycler/ 回收站
    /edit/category/ 编辑分类
    /add/category/ 新增分类
    /delete/category/ 删除分类
    /backend/category/2/  展示某分类对应的文章
    
    1. 文章增删改查;
       文章的删除:
         1)移动到回收站
         2)完全删除
    2. 前端编辑器-kindeditor富文本编辑器
       1) 处理xss攻击
       2)文章摘要获取text文本前150字符
       3)编辑器上传图片
    
    3. 文章分类的增删 
    
    4. 使用bs4模块实现:
       1. 防止xss攻击 
       XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些代码通常是js。所以需要用到bs4清除用户编辑内容中的js代码。
       2. 获取文章的前150个text字符作为desc
       因为文章提交时是html类型,包括标签,需要使用bs4获取到纯文本。
    
    5. category与article表添加了delete的布尔值字段。
    

    效果:

    后台管理主页面

    主页面

    新增文章

    编辑旧文章

    回收站两个功能: 1. 移出回收站 2. 删除

    编辑分类: 删除与新增两个功能


    展示某分类对应的文章

    代码:
    路由层 BBS\urls.py

    点击查看代码
        # 后台管理
        url(r'^backend/$',views.backend),
        # 后台展示分类文章
        url(r'^backend/(?P<condition>category)/(?P<param>\d+)/',views.backend),
        # 后台回收站文章展示
        url(r'^backend/recycler/',views.recycler),
        # 添加文章
        url(r'^add/article/',views.add_article),
        # 添加文章
        url(r'^change/article/(?P<param>\d+)/',views.add_article),
        # 删除文章
        url(r'^delete/article/(?P<param>\d+)/',views.delete_article),
        # 编辑|新增|删除分类
        url(r'^(?P<action>edit|add|delete)/category/',views.edit_category),
    
        # 上传图片
        url(r'^upload_img/',views.upload_img),
    

    视图层 app01\views.py

    点击查看代码
    # 后台管理主页面
    def backend(request,**kwargs):
        category_list = models.Category.objects.filter(blog=request.user.blog,delete=0).annotate(article_c=Count("article__pk")).values_list("pk","name","article_c")
        tag_list = models.Tag.objects.filter(blog=request.user.blog)
        # 展示该用户全部文章
        article_obj_list = models.Article.objects.filter(blog=request.user.blog,delete=0)
        
        if kwargs:
            param = kwargs['param']
            # 展示某个分类的文章
            article_obj_list = models.Article.objects.filter(blog=request.user.blog,category_id=param,delete=0)
    
        return render(request,'backend/backend.html',locals())
    
    from bs4 import BeautifulSoup
    
    # 新增文章 或 编辑旧文章的编辑页面 与 数据提交
    def add_article(request,**kwargs):
        title = ''
        content = ''
        category_id = ''
        tag_id_list = ''
        category_list = models.Category.objects.filter(blog=request.user.blog,delete=0).annotate(article_c=Count("article__pk")).values_list("pk","name","article_c")
        tag_list = models.Tag.objects.filter(blog=request.user.blog)
    
        # 如果 kwargs有值就是编辑旧的文章,需要获取旧文章数据展示到编辑页面
        if kwargs:
            article_id = kwargs['param']
            article_obj = models.Article.objects.filter(pk=article_id).first()
            title = article_obj.title
            content = article_obj.content
            category_id = article_obj.category_id
            tags_list = article_obj.tags.all()
            tag_id_list = [i.pk for i in tags_list]
        # 获取提交的文章数据保存到数据库
        if request.method == 'POST':
            title = request.POST.get('title')
            content = request.POST.get('content')
            category_id = request.POST.get('category_id')
            tag_id_list = request.POST.getlist('tag_id')
            article_id = request.POST.get('active')
            # bs4的使用   'html.parser'为python自带的html解析器
            soup = BeautifulSoup(content,'html.parser')
            # 获取到所有标签
            content_eles = soup.find_all()
            # content_tag是一个一个的标签对象
            for content_ele in content_eles:
                # 将提交内容中的script标签删掉,防止xss攻击
                if content_ele.name == 'script':
                    content_ele.decompose()
            # 获取到内容中的纯文本,截取前150个字符为desc字段
            # print(type(soup))  # <class 'bs4.BeautifulSoup'> 存储上要用str(soup) 
            desc = soup.text[0:150]
            # 新增文章
            if article_id == '0':
                article_obj = models.Article.objects.create(
                    title=title,
                    desc=desc,
                    content=str(soup),
                    blog=request.user.blog,
                    category_id=category_id,
                    )
            # 修改文章
            else:
                article_obj = models.Article.objects.filter(pk=article_id).first()
                article_obj.title=title
                article_obj.desc=desc
                article_obj.content=str(soup)
                article_obj.category_id=category_id
                article_obj.save()
                models.ArticleToTag.objects.filter(article_id=article_id).delete()
            # 存储tag bulk_create批量创建tag数据
            articleToTag_obj_list = []
            for tag_id in tag_id_list:
                articleToTag_obj = models.ArticleToTag(
                    article = article_obj,
                    tag_id = tag_id,
                )
                articleToTag_obj_list.append(articleToTag_obj)
            
            models.ArticleToTag.objects.bulk_create(articleToTag_obj_list)
            return redirect('/backend/')
        # 展示编辑页面
        return render(request,'backend/add_article.html',locals())
    
    import time
    # 文本编辑器上传图片保存
    def upload_img(request):
        # 富文本编辑器上传图片返回固定格式数据
        back_dic={
            "error" : 0,
            "url" : ""
        }
        img = request.FILES.get('imgFile')
        # 获取到文件的扩展名
        file_type = img.name.split('.')[-1]
        # 拼接不会重复的文件名
        filename=str(time.time()).replace('.','_')+'.%s' %file_type
        # 将文件保存到对应文件夹
        with open('media/img/%s' %filename,'wb') as f:
            for line in img:
                f.write(line)
    
        back_dic['url']='/media/img/%s' %filename
        return JsonResponse(back_dic)
    
    # 删除文章 包括完全删除与放到回收站两种
    def delete_article(request,param):
        back_dic = {'code':1000}
        if request.method == 'POST':
            # from_p区分是/backend/页面删除还是回收站删除
            from_p = request.POST.get('from')
            delete_id = request.POST.get('delete_id')
            if from_p == "backend":
                models.Article.objects.filter(pk=delete_id).update(delete='1')
            elif from_p == "recycler":
                models.Article.objects.filter(pk=delete_id).delete()
        return JsonResponse(back_dic)
    
    # 回收站 
    def recycler(request):  
        article_obj_list = models.Article.objects.filter(blog=request.user.blog,delete=1)
        category_list = models.Category.objects.filter(blog=request.user.blog,delete=0).annotate(article_c=Count("article__pk")).values_list("pk","name","article_c")
        #移出回收站
        back_dic = {'code':1000}
        if request.method == 'POST':
            move_id = request.POST.get('move_id')
            models.Article.objects.filter(pk=move_id).update(delete='0')
            return JsonResponse(back_dic)
        # 展示回收站文章
        return render(request,'backend/recycler.html',locals())
    
    # 编辑分类 包括 展示/删除/新增
    def edit_category(request,action):
        category_list = models.Category.objects.filter(blog=request.user.blog,delete=0).annotate(article_c=Count("article__pk")).values_list("pk","name","article_c")
        if request.method == 'POST':
            back_dic = {"code":1000,'url':'/edit/category/'}
            # 删除
            if action == 'delete':
                delete_id = request.POST.get('delete_id')
                models.Category.objects.filter(pk=delete_id).update(delete=1)
            # 新增
            elif action == 'add':
                category_name = request.POST.get('category_name')
                if category_name:
                    models.Category.objects.create(name=category_name,blog=request.user.blog)
                else:
                    back_dic['code'] = 1001
                    back_dic['msg'] = "类名不能为空"
            return JsonResponse(back_dic)
        # 展示分类        
        return render(request,'backend/show_category.html',locals())
    

    模板层 后台页面放到 templates/backend/下

    模板页面 templates\backend\backend_base.html

    点击查看代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script>
        {% load static %}
        <link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
        <script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
        <script src="{% static '/js/mysetup.js' %}"></script>
        <title>BBS</title>
        <script charset="utf-8" src="{% static 'others/kindeditor/kindeditor-all.js' %} "></script>
        {% block css_part %}
          
        {% endblock css_part %}
    </head>
    <body>
        <!-- 导航条 -->
        <nav class="navbar navbar-inverse">
            <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="/home/">BBS</a>
              </div>
          
              <!-- Collect the nav links, forms, and other content for toggling -->
              <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                  <li class="active"><a href="#">博客 <span class="sr-only">(current)</span></a></li>
                  <li><a href="#">文章</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="#">Action</a></li>
                      <li><a href="#">Another action</a></li>
                      <li><a href="#">Something else here</a></li>
                      <li role="separator" class="divider"></li>
                      <li><a href="#">Separated link</a></li>
                      <li role="separator" class="divider"></li>
                      <li><a href="#">One more separated link</a></li>
                    </ul>
                  </li>
                </ul>
                <form class="navbar-form navbar-left">
                  <div class="form-group">
                    <input type="text" class="form-control" placeholder="Search">
                  </div>
                  <button type="submit" class="btn btn-default">Submit</button>
                </form>
                <ul class="nav navbar-nav navbar-right">
                    {% if request.user.is_authenticated %}
                            <li><a href="/backend/">后台管理</a></li>
                            <li><a href="/{{ request.user.username }}/">{{ request.user.username }}</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">
                                <!-- 添加 data-toggle="modal" data-target=".bs-example-modal-lg属性后点击此标签弹出模态框 -->
                                <li><a href="" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
                                <!-- 添加 data-toggle="modal" data-target=".vatar-modal-lg" 点击弹出更换头像的模态框 -->
                                <li><a href="" data-toggle="modal" data-target=".vatar-modal-lg">更换头像</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="{% url 'logout' %}">退出登录</a></li>
                              </ul>
                            </li>
                    {% else %}  
                        <li><a href="/login/">登录</a></li>
                        <li><a href="/register/">注册</a></li>    
                    {% endif %}
                    
                    </ul>
                  </li>
                </ul>
              </div><!-- /.navbar-collapse -->
            </div><!-- /.container-fluid -->
          </nav>
    
        <!-- 修改密码模态框 将data-toggle="modal" data-target=".bs-example-modal-lg"加到点击密码的a标签-->
        <!-- <button type="button" class="btn btn-primary" data-toggle="modal" data-target=".bs-example-modal-lg" >Large modal</button> -->
        <!-- 这里的class中还需要加上bs-example-modal-lg才能弹出 -->
        <div class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
          <div class="modal-dialog modal-lg" role="document">
            <div class="modal-content">
                <h2 class="text-center">修改密码</h2>
                <div class="row">
                    <div class="col-md-8 col-md-offset-2">
                        <div class="form-group">
                            <label for="username">用户名</label>
                            <input type="text" class="form-control" id="username" value="{{ request.user.username }}" disabled>
                          </div>
                          <div class="form-group">
                            <label for="old_password">原密码</label>
                            <input type="password" class="form-control" id="old_password">
                          </div>
                          <div class="form-group">
                            <label for="new_password">新密码</label>
                            <input type="password" class="form-control" id="new_password">
                          </div>
                          <div class="form-group">
                            <label for="confirm_password">确认密码</label>
                            <input type="password" class="form-control" id="confirm_password">
                          </div>
                        <div class="modal-footer">
                            <span class="pull-left" id="prompt" style="color: red;"></span>
                            <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                            <button type="button" class="btn btn-primary" id="change_pwd_btn">确认</button>
                        </div>
                    </div>
                </div>
            </div>
          </div>
        </div>
        <!-- 修改头像模态框 -->
        <div class="modal fade vatar-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
            <div class="modal-dialog modal-lg" role="document">
              <div class="modal-content">
                  <h2 class="text-center">更换头像</h2>
                  <div class="row">
                      <div class="col-md-8 col-md-offset-2">
                        <label for="id_avatar">
                            头像:
                            <img src="/media/{{ request.user.avatar }}" alt="" style=" 150px;" id="img_avatar">
                            <!-- <img src="{% static 'img/default.png' %}" alt="" id='myimg' style=" 100px;margin-left: 10px;"> -->
                        </label>
                        <input type="file" id="id_avatar" style="display:none">
                        <div class="modal-footer">
                            <span class="pull-left" id="prompt" style="color: red;"></span>
                            <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                            <button type="button" class="btn btn-primary" id="change_vatar_btn">确认</button>
                        </div>
                      </div>
                  </div>
              </div>
            </div>
        </div>
    
        <!-- 2-10布局 -->
        <div class="row">
            <div class="col-md-2">
                <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                    <div class="panel panel-default">
                      <div class="panel-heading" role="tab" id="headingOne">
                        <h4 class="panel-title">
                          <a role="button" data-parent="#accordion" href="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
                            操作
                          </a>
                        </h4>
                      </div>
                      <div id="collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne">
                        <div class="panel-body">
                            <a href="/add/article/">添加文章</a>
                            
                        </div>
                        <div class="panel-body">
                            <a href="">草稿箱</a>
                        </div>
                        <div class="panel-body">
                            <a href="/backend/recycler/">回收站</a>
                        </div>
                      </div>
                    </div>
                    <div class="panel panel-default">
                        <div class="panel-heading" role="tab" id="headingTwo">
                          <h4 class="panel-title">
                            <a class="" role="button" data-parent="#accordion" href="#collapseTwo" aria-expanded="true" aria-controls="collapseTwo">
                              分类
                            </a>
                          </h4>
                        </div>
                        <div id="collapseTwo" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingTwo" aria-expanded="true">
                          <div class="panel-body">
                            <a href="/edit/category/">编辑分类</a>
                          </div>
                          {% for category in category_list %}
                          <div class="panel-body">
                            <a href="/backend/category/{{category.0}}">{{category.1}}({{category.2}})</a>
                          </div>
                          {% endfor %}
                        </div>
                    </div>
                  </div>
    
            </div>
            <div class="col-md-10">
                <!-- 标签页 -->
                <div>
    
                    <!-- Nav tabs -->
                    <ul class="nav nav-tabs" role="tablist">
                      <li role="presentation" class="active"><a href="#home" aria-controls="home" role="tab" data-toggle="tab">文章</a></li>
                      <li role="presentation"><a href="#profile" aria-controls="profile" role="tab" data-toggle="tab">评论</a></li>
                      <li role="presentation"><a href="#messages" aria-controls="messages" role="tab" data-toggle="tab">标签</a></li>
                      <li role="presentation"><a href="#settings" aria-controls="settings" role="tab" data-toggle="tab">文件</a></li>
                    </ul>
                  
                    <!-- Tab panes -->
                    <div class="tab-content">
                      <div role="tabpanel" class="tab-pane active" id="home">
                          {% block article %}
                            
                          {% endblock article %}
                      </div>
                      <div role="tabpanel" class="tab-pane" id="profile">
                        {% block comment %}
                            
                        {% endblock comment %}
                      </div>
                      <div role="tabpanel" class="tab-pane" id="messages">
                        {% block tag %}
                            
                        {% endblock tag %}
                      </div>
    
                      <div role="tabpanel" class="tab-pane" id="settings">
                        {% block file %}
                            
                        {% endblock file %}
                      </div>
                    </div>
                  
                  </div>
            </div>
        </div>
    
    
    
    
    
        <script>
            // 修改密码使用ajax提交信息
            $('#change_pwd_btn').click(function(){
                $.ajax({
                    url: "{% url 'setPassword' %}",
                    method: 'post',
                    data: {'old_password':$('#old_password').val(),'new_password':$('#new_password').val(),'confirm_password':$('#confirm_password').val()},
                    success: function(args){
                        if (args.code == 1000){
                            $('#prompt').text(args.msg).attr('style','color: blue');
                            window.location.reload()
                            }
                        else {$('#prompt').text(args.msg)}
                    }
                })
            })
    
            // 修改头像处上传后更新展示
            $('#id_avatar').change(function(){
                // 文件阅读器对象
                // 1. 生成一个文件阅读器
                let myFileReaderObj = new FileReader();
                // 2. 获取用户上传的头像文件
                let fileObj = $(this)[0].files[0];
                // 3. 将文件对象交给阅读器处理   
                myFileReaderObj.readAsDataURL(fileObj)  // 异步 IO操作 所以这里文件还没读完就会执行下一行代码
                // 4. 利用阅读器将文件展示到前端页面   
                myFileReaderObj.onload = function(){  // onload 等待myFileReaderObj对象的任务都执行完成才执行
                    $('#img_avatar').attr('src',myFileReaderObj.result)
                }
            })
            // 修改头像提交
            $('#change_vatar_btn').click(function(){
                let formDataObj = new FormData();
                formDataObj.append('new_avatar',$('#id_avatar')[0].files[0])
                $.ajax({
                    url: "{% url 'changeAvatar' %}",
                    method: 'post',
                    data: formDataObj,
                    contentType: false,
                    processData: false,
                    success: function(args){
                        if (args.code==1000){window.location.reload()}
                        else{window.location.reload()}   
                    },
                })
            })
        </script>
    {% block js_part %}
      
    {% endblock js_part %}
    </body>
    </html>
    

    templates\backend\add_article.html

    点击查看代码
    {% extends "backend\backend_base.html" %}
    
    {% block css_part %}
        <style>
            h4 {
                font-weight: bold;
                border-style: solid none dashed none;
                border-thin;
                border-color:rgb(150, 144, 144);
                padding: 10px;
                background-color:rgb(206, 202, 202)
            }
        </style>
    {% endblock css_part %}
    {% block article %}
        <div style="margin-right: 30px;background-color: rgb(228, 228, 228);">
            <h4 >添加文章</h4>
            <form action="/add/article/" method="post">
                {% csrf_token %}
    
                <p style="margin-top: 20px;margin-left: 10px; font-weight: bold;font-size: large;">标题</p>
                <!-- 判断是更改 还是 新增 -->
                {% if title %}
                <input type="text" name="active" value="{{article_obj.pk}}" style="display: none;">
                {% else %}
                <input type="text" name="active" value="0" style="display: none;">
                {% endif %}
    
                <input type="text" name="title" value="{{ title }}" class="form-control" style="font-weight: bold; border-style: solid ;border-medium;height: 40px;">
                <!-- 编辑框 -->
                <textarea id="edit" name="content" value="{{ content }}">
                </textarea>
                <h4 >文章分类</h4>
                {% for category_obj in category_list %}
                    {% if category_obj.0 == category_id %}
                        <label><input type="radio" name="category_id" value="{{ category_obj.0 }}" checked>{{ category_obj.1 }}</label>&nbsp;&nbsp;&nbsp;&nbsp;
                    {% else %}
                        <label><input type="radio" name="category_id" value="{{ category_obj.0 }}" >{{ category_obj.1 }}</label>&nbsp;&nbsp;&nbsp;&nbsp;
                    {% endif %}
                {% endfor %}
                
                <h4>文章标签</h4>
                {% for tag_obj in tag_list %}
                {% if tag_obj.pk in tag_id_list %}
                    <label><input type="checkbox" name="tag_id" value="{{ tag_obj.pk }}" checked> {{ tag_obj.name }}</label>&nbsp;&nbsp;&nbsp;&nbsp;
                {% else %}
                    <label><input type="checkbox" name="tag_id" value="{{ tag_obj.pk }}"> {{ tag_obj.name }}</label>&nbsp;&nbsp;&nbsp;&nbsp;
                {% endif %}
                
                {% endfor %}
                <hr>
                <div ><button type="submit" class="btn btn-primary pull-right">&nbsp;发布&nbsp;</button></div>
            </form>
        </div>
        
    {% endblock article %}
    
    
    {% block js_part %}
    
    <script>
        
        // 加载富文本编辑器
        KindEditor.ready(function(K) {
                window.editor = K.create('#edit',{
                     '100%',         // 编辑框宽度设置       
                    height: '600px',       // 编辑框高度设置 
                    resizeType: 1,         // resizeType为1,表示也没事编辑框高度可调,宽度不可变
    
                    // 以下都是上传图片/文件相关配置
                    uploadJson : '/upload_img/',  // 上传文件的url
                    extraFileUploadParams : {
                        'csrfmiddlewaretoken':'{{ csrf_token }}',  // 额外携带的参数
                    }
                })
                window.editor.html($('textarea[name=content]').attr('value'))
        });
    </script>
    
    {% endblock js_part %}
    

    templates\backend\backend.html

    点击查看代码
    {% extends "backend\backend_base.html" %}
    
    {% block article %}
        <br>
        <table class="table table-striped">
        <thead>
            <tr>
                <th>标题</th>
                <th>发布时间</th>
                <th>评论数</th>
                <th>点赞数</th>
                <th>操作</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            {% for article_obj in article_obj_list %}
            <tr>
                <td><a href="/{{article_obj.blog.userinfo}}/article/{{article_obj.pk}}">{{article_obj.title}}</a></td>
                <td>{{article_obj.create_time|date:'Y-m-d'}}</td>
                <td>{{article_obj.comment_num}}</td>
                <td>{{article_obj.up_num}}</td>
                <td><a href="/change/article/{{ article_obj.pk }}">编辑</a></td>
                <td><a class="delete_article" article_id="{{ article_obj.pk }}">删除</a></td>
            </tr>
            {% endfor %}
    
        </tbody>
        </table>
    {% endblock article %}
    
    {% block js_part %}
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.css" integrity="sha512-f8gN/IhfI+0E9Fc/LKtjVq4ywfhYAVeMGKsECzDUHcFJ5teVwvKTqizm+5a84FINhfrgdvjX8hEJbem2io1iTA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.js" integrity="sha512-XVz1P4Cymt04puwm5OITPm5gylyyj5vkahvf64T8xlt/ybeTpz4oHqJVIeDtDoF5kSrXMOUmdYewE4JS/4RWAA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script>
        $(".delete_article").click(function(){
            // 在按钮的属性中定义该条数据的主键,再拿到主键值,传递给后端
            let delete_id =  $(this).attr('article_id') 
            url = '/delete/article/'+delete_id+'/'
            from = 'backend'
            swal({
                title: "请再次确认?",               // 弹出框标题
                text: "删除后可以在回收站找回",      // 弹出框正文
                type: "warning",                   // 弹出框类型,图标显示不同,有四种:warning,error,success,info
                showCancelButton: true,            // 显示取消按钮(确认按钮默认显示)
                cancelButtonClass: "btn-success",  // 取消按钮为btn-success样式,蓝色
                cancelButtonText: "取消",          // 取消按钮显示内容
                confirmButtonClass: "btn-danger",  // 确认按钮样式,红色
                confirmButtonText: "删除",         // 确认按钮显示内容
                closeOnConfirm: false,             // false表示点击确认按钮不关闭弹框
                },
                function(){                        // 点击确认后会触发这个function,点击取消后弹窗会关闭,不触发。
                    $.ajax({
                        url: url,
                        type: 'post',
                        data: {'delete_id':delete_id,'from':from},
                        success: function(args){
                            if (args.code === 1000){
                                // 弹窗显示已删除,点击确认后刷新页面
                                swal({title:"已删除!!!",},function(){window.location.reload()})
                                }
                            else {
                                swal("未知错误!!!")}
                        }
                    })
                });
        })
    </script> 
    {% endblock js_part %}
    

    templates\backend\recycler.html

    点击查看代码
    {% extends "backend\backend_base.html" %}
    
    {% block article %}
        <br>
        <table class="table table-striped">
        <thead>
            <tr>
                <th>标题</th>
                <th>发布时间</th>
                <th>评论数</th>
                <th>点赞数</th>
                <th>操作</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            {% for article_obj in article_obj_list %}
            <tr>
                <td><a href="/{{article_obj.blog.userinfo}}/article/{{article_obj.pk}}">{{article_obj.title}}</a></td>
                <td>{{article_obj.create_time|date:'Y-m-d'}}</td>
                <td>{{article_obj.comment_num}}</td>
                <td>{{article_obj.up_num}}</td>
                <td><a class="moveout" article_id="{{ article_obj.pk }}">移出回收站</a></td>
                <td><a class="delete_article" article_id="{{ article_obj.pk }}">删除</a></td>
            </tr>
            {% endfor %}
    
        </tbody>
        </table>
    {% endblock article %}
    
    {% block js_part %}
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.css" integrity="sha512-f8gN/IhfI+0E9Fc/LKtjVq4ywfhYAVeMGKsECzDUHcFJ5teVwvKTqizm+5a84FINhfrgdvjX8hEJbem2io1iTA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.js" integrity="sha512-XVz1P4Cymt04puwm5OITPm5gylyyj5vkahvf64T8xlt/ybeTpz4oHqJVIeDtDoF5kSrXMOUmdYewE4JS/4RWAA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script>
        $(".delete_article").click(function(){
            // 在按钮的属性中定义该条数据的主键,再拿到主键值,传递给后端
            let delete_id =  $(this).attr('article_id') 
            url = '/delete/article/'+delete_id+'/'
            from = 'recycler'
            swal({
                title: "请再次确认?",               // 弹出框标题
                text: "删除后将无法找回",      // 弹出框正文
                type: "warning",                   // 弹出框类型,图标显示不同,有四种:warning,error,success,info
                showCancelButton: true,            // 显示取消按钮(确认按钮默认显示)
                cancelButtonClass: "btn-success",  // 取消按钮为btn-success样式,蓝色
                cancelButtonText: "取消",          // 取消按钮显示内容
                confirmButtonClass: "btn-danger",  // 确认按钮样式,红色
                confirmButtonText: "删除",         // 确认按钮显示内容
                closeOnConfirm: false,             // false表示点击确认按钮不关闭弹框
                },
                function(){                        // 点击确认后会触发这个function,点击取消后弹窗会关闭,不触发。
                    $.ajax({
                        url: url,
                        type: 'post',
                        data: {'delete_id':delete_id,'from':from},
                        success: function(args){
                            if (args.code === 1000){
                                // 弹窗显示已删除,点击确认后刷新页面
                                swal({title:"已删除!!!",},function(){window.location.reload()})
                                }
                            else {
                                swal("未知错误!!!")}
                        }
                    })
                });
        })
    
        $('.moveout').click(function(){
            let move_id =  $(this).attr('article_id')
            $.ajax({
                url: '/backend/recycler/',
                type: 'post',
                data: {'move_id':move_id},
                success: function(args){
                    if (args.code === 1000){
                        window.location.href="/backend/"
                    }
                    else {
                        swal("未知错误!!!")
                    }
                }
            })
        })
    </script> 
    {% endblock js_part %}
    

    templates\backend\show_category.html

    点击查看代码
    {% extends "backend\backend_base.html" %}
    
    {% block article %}
        <br>
        <button class="btn-primary btn pull-right" style="margin-right: 50px;" data-toggle="modal" data-target=".category-modal-lg"> 新增 </button>
        <br>
        <table class="table table-striped">
        <thead>
            <tr>
                <th>分类</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            {% for category_obj in category_list %}
            <tr>
                <td>{{category_obj.1}}</td>
                <td><a class="delete_category" category_id="{{ category_obj.0 }}">删除</a></td>
            </tr>
            {% endfor %}
    
        </tbody>
        </table>
        <!-- 新增分类模态框 -->
        <div class="modal fade category-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
            <div class="modal-dialog modal-lg" role="document">
              <div class="modal-content">
                  <h2 class="text-center">新增分类</h2>
                  <div class="row">
                      <div class="col-md-8 col-md-offset-2">
                            <div class="form-group">
                              <label for="new_category">新类名</label>
                              <input type="text" class="form-control" id="new_category">
                            </div>
                          <div class="modal-footer">
                              <span class="pull-left" id="prompt" style="color: red;"></span>
                              <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                              <button type="button" class="btn btn-primary" id="add_category">确认</button>
                          </div>
                      </div>
                  </div>
              </div>
            </div>
          </div>
    {% endblock article %}
    
    {% block js_part %}
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.css" integrity="sha512-f8gN/IhfI+0E9Fc/LKtjVq4ywfhYAVeMGKsECzDUHcFJ5teVwvKTqizm+5a84FINhfrgdvjX8hEJbem2io1iTA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.js" integrity="sha512-XVz1P4Cymt04puwm5OITPm5gylyyj5vkahvf64T8xlt/ybeTpz4oHqJVIeDtDoF5kSrXMOUmdYewE4JS/4RWAA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script>
        $(".delete_category").click(function(){
            // 在按钮的属性中定义该条数据的主键,再拿到主键值,传递给后端
            let delete_id =  $(this).attr('category_id') 
            swal({
                title: "请再次确认?",               // 弹出框标题
                text: "删除后将无法找回",      // 弹出框正文
                type: "warning",                   // 弹出框类型,图标显示不同,有四种:warning,error,success,info
                showCancelButton: true,            // 显示取消按钮(确认按钮默认显示)
                cancelButtonClass: "btn-success",  // 取消按钮为btn-success样式,蓝色
                cancelButtonText: "取消",          // 取消按钮显示内容
                confirmButtonClass: "btn-danger",  // 确认按钮样式,红色
                confirmButtonText: "删除",         // 确认按钮显示内容
                closeOnConfirm: false,             // false表示点击确认按钮不关闭弹框
                },
                function(){                        // 点击确认后会触发这个function,点击取消后弹窗会关闭,不触发。
                    $.ajax({
                        url: '/delete/category/',
                        type: 'post',
                        data: {'delete_id':delete_id},
                        success: function(args){
                            if (args.code === 1000){
                                // 弹窗显示已删除,点击确认后刷新页面
                                swal({title:"已删除!!!",},function(){window.location.reload()})
                                }
                            else {
                                swal("未知错误!!!")}
                        }
                    })
                });
        })
    
    
    // 新增分类
    $('#add_category').click(function(){
                $.ajax({
    
                    url: "/add/category/",
                    method: 'post',
                    data: {'category_name':$('#new_category').val()},
                    success: function(args){
                        if (args.code == 1000){
                            window.location.href=args.url
                            }
                        else {$('#prompt').text(args.msg)}
                    }
                })
            })
    </script> 
    
    
    {% endblock js_part %}
    

    后续bug调试

    1. 用户注册时,要自动创建对应的blog
    2. tag标签没有写添加与删除
    3. 草稿箱功能以及后台管理的评论、标签、文件都没写
    4. 导航条很多选项没有用到
    ...
    

    完整代码(这个版本只有主要功能,且还有bug):
    https://files.cnblogs.com/files/huandada/BBS.zip

  • 相关阅读:
    Cygwin/WSL专用Shell函数,restartps:带命令行参数重启某进程
    Cygwin/WSL一键备份或导入Windows计划任务实用函数示例(backupschtasks、restoreschtasks)
    Cygwin/WSL调用Windows schtasks命令操作Windows计划任务系列函数(查询、启用、禁用、删除)
    Cygwin/MSYS2文件夹书签功能函数,调用Windows资源管理器快速打开文件夹;
    Cygwin/WSL专用函数wmicps:借助Windows WMIC组件从命令行获取某进程的命令行参数及其关联信息;
    Windows命令行窗口查询DLNA读取的多媒体文件路径和读取的进度;
    《The way to go学习笔记》第三章
    android平台的图文上下滚动与左右滚动效果
    架构第二周作业
    架构第一周练习题
  • 原文地址:https://www.cnblogs.com/huandada/p/16029877.html
Copyright © 2020-2023  润新知