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="width: 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="width: 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="width: 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="width: 150px;" id="img_avatar">
                        <!-- <img src="{% static 'img/default.png' %}" alt="" id='myimg' style="width: 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-width: 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-width: 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="width: 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="width: 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;
    width: 125px;
    text-align: center;
    margin-top: 10px;
    }
    .diggit {
        float: left;
        width: 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;
        width: 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;
    width: 125px;
    text-align: center;
    margin-top: 10px;
    }
    .diggit {
        float: left;
        width: 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;
        width: 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',{
                width: '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="width: 150px;" id="img_avatar">
                        <!-- <img src="{% static 'img/default.png' %}" alt="" id='myimg' style="width: 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-width: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-width: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',{
                width: '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

posted @ 2022-03-20 14:21  huandada  阅读(267)  评论(0编辑  收藏  举报