首页 > 其他分享 >BBS项目

BBS项目

时间:2023-08-28 21:36:29浏览次数:22  
标签:项目 models list get request article BBS id

一、开发流程和数据库表结构设计

1、项目开发流程

-立项
-需求分析
-原型图
-切图
-技术选型,数据库架构设计
-前后端协同开发
-上线测试服务器联调
-测试
-修改bug
-上线运行

2、基本需求:

1 注册功能
 -使用forms组件
 -使用Ajax提交数据信息
 -支持上传头像

2 登录功能
 -使用Ajax提交数据信息
 -渲染用户名密码不符合要求的错误信息

3 博客首页
 -列出所有文章
 -文章旁边可以显示作者头像
 -文章旁边可以看到文章的发布时间
 -文章旁边可以看到点赞点踩的数量

4 个人站点
 -左侧使用inclusion_tag对标签和分类进行过滤
 -展示个人站点中的文章

5 文章详情页
 -点赞点踩功能(同一用户只能点一次)
 -评论功能(包含子评论)

6 后台管理功能
 -展示登录用户的所有文章
 -文章新增(防止xss攻击)
另有部分拓展需求我们会在每一节中详细列出

二、数据库表结构设计

1、一般来说,设计一个数据库的表结构一般要注意以下三点:

1 把项目的需求转化为一个个数据库中的表
2 探寻表与表之间的关联关系
3 牢记以下原则:
能用多对多关联关系就尽量不要用一对多关联关系,能用一对多关联关系就尽量别用一对一(原因是为了减少耦合)

2、models 文件

from django.db import models

# Create your models here.
"""
先写普通字段
之后再写外键字段
"""
from django.contrib.auth.models import AbstractUser


class UserInfo(AbstractUser):
    phone = models.BigIntegerField(verbose_name='手机号', null=True, blank=True)
    '''
    null=True:数据库中phone字段可以为空, blank=True:后台管理系统可以为空。
    '''
    # 头像
    avatar = models.FileField(upload_to='avatar/', default='avatar/default.png', verbose_name='用户头像')
    """
    给avatar字段传文件对象 该文件会自动存储到avatar文件下 然后avatar字段只保存文件路径avatar/default.png
    """
    create_time = models.DateField(auto_now_add=True)

    blog = models.OneToOneField(to='Blog', null=True, on_delete=models.CASCADE)

    class Meta:
        verbose_name = verbose_name_plural = "用户信息"


class Blog(models.Model):
    site_name = models.CharField(verbose_name='站点名称', max_length=32)
    site_title = models.CharField(verbose_name='站点标题', max_length=32)
    # 简单模拟 带你认识样式内部原理的操作
    site_theme = models.CharField(verbose_name='站点样式', max_length=64)  # 存css/js的文件路径

    def __str__(self):
        return self.site_name

    class Meta:
        verbose_name = verbose_name_plural = '博客站点'


class Category(models.Model):
    name = models.CharField(verbose_name='文章分类', max_length=32)
    blog = models.ForeignKey(to='Blog', null=True, on_delete=models.CASCADE)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = verbose_name_plural = '分类'


class Tag(models.Model):
    name = models.CharField(verbose_name='文章标签', max_length=32)
    blog = models.ForeignKey(to='Blog', null=True, on_delete=models.CASCADE)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = verbose_name_plural = '标签'


class Article(models.Model):
    title = models.CharField(verbose_name='文章标题', max_length=64)
    desc = models.CharField(verbose_name='文章简介', max_length=255)
    # 文章内容有很多 一般情况下都是使用TextField
    content = models.TextField(verbose_name='文章内容')
    create_time = models.DateField(auto_now_add=True)

    # 数据库字段设计优化
    up_num = models.IntegerField(verbose_name='点赞数', default=0)
    down_num = models.IntegerField(verbose_name='点踩数', default=0)
    comment_num = models.IntegerField(verbose_name='评论数', default=0)

    # 外键字段
    blog = models.ForeignKey(to='Blog', null=True, on_delete=models.CASCADE)
    category = models.ForeignKey(to='Category', null=True, on_delete=models.CASCADE)
    tags = models.ManyToManyField(to='Tag',
                                  through='Article2Tag',
                                  through_fields=('article', 'tag')
                                  )

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = verbose_name_plural = '文章'


class Article2Tag(models.Model):
    article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
    tag = models.ForeignKey(to='Tag', on_delete=models.CASCADE)


class UpAndDown(models.Model):
    user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
    article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
    is_up = models.BooleanField()  # 传布尔值 存0/1

    class Meta:
        verbose_name = verbose_name_plural = '点赞和点踩'


class Comment(models.Model):
    user = models.ForeignKey(to='UserInfo', null=True, on_delete=models.CASCADE)
    article = models.ForeignKey(to='Article', null=True, on_delete=models.CASCADE)
    content = models.CharField(verbose_name='评论内容', max_length=255)
    comment_time = models.DateTimeField(verbose_name='评论时间', auto_now_add=True)
    # 自关联
    parent = models.ForeignKey(to='self', null=True, on_delete=models.CASCADE)  # 有些评论就是根评论

    class Meta:
        verbose_name = verbose_name_plural = '评论'

补充:

1、自关联

自关联是指一个模型与自身建立关联的情况。这种模型之间的关系被称为自关联或递归关联。自关联在许多情况下都非常有用,可以用来建模层次结构、分类、评论系统等。

使用场景:

  1. 层次结构和分类: 一个典型的使用场景是在建模层次结构,例如组织机构、地理位置等。一个模型实例可能会与同一模型的其他实例形成父子关系,从而形成层次结构。

  2. 评论系统: 在建立评论系统时,评论可以被回复,这就需要在评论模型中建立自关联,以便一个评论可以有多个回复。

  3. 社交网络中的关注系统: 在社交网络中,用户之间可以相互关注,这也是一个自关联的例子,其中用户与其他用户建立关联。

  4. 导航和菜单: 如果你需要建立一个多级导航或菜单系统,那么自关联可以用于构建不同级别的导航项。

实现自关联:

要在Django中实现自关联,你需要在模型中使用ForeignKeyOneToOneField字段,指向自身的模型。

这里允许字段为空,null=True,第一次评论没有根评论

 parent = models.ForeignKey(to='self', null=True, on_delete=models.CASCADE)

app01 views汇总

import json
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse
from app01 import models
from django.conf import settings
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO, StringIO
from django.db.models import Count
from django.db.models.functions import TruncMonth, TruncWeek, TruncYear, TruncDay
import random


def get_md5(password):
    import hashlib
    m = hashlib.md5()
    m.update((password + settings.SECRET_KEY).encode('utf8'))
    return m.hexdigest()


# @login_required(login_url='/login/')
def home(request):
    article_list = models.Article.objects.all()
    user = request.session.get('username')
    # print(user)
    try:
        user_info_obj = models.UserInfo.objects.get(username=user)
        avatar = user_info_obj.avatar
        # print(avatar)
    except models.UserInfo.DoesNotExist:
        avatar = '/static/img/default.jpg'  # 默认头像路径

    return render(request, 'home.html', locals())


def get_random():
    return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)


def get_code(request):
    # 最终步骤4:写图片验证码
    img_obj = Image.new('RGB', (430, 35), get_random())
    img_draw = ImageDraw.Draw(img_obj)  # 产生一支画笔对象
    img_font = ImageFont.truetype('static/font/maoti.ttf', 30)  # 字体样式 大小。

    # 随机验证码  五位数的随机验证码  数字 小写字母 大写字母
    code = ''
    for i in range(5):  # 随机验证码5个随机数
        random_upper = chr(random.randint(65, 90))
        random_lower = chr(random.randint(97, 122))
        random_int = str(random.randint(0, 9))
        # 从上面三个里面随机选择一个
        tmp = random.choice([random_lower, random_upper, random_int])
        # 将产生的随机字符串写入到图片上
        """
        为什么一个个写而不是生成好了之后再写
        因为一个个写能够控制每个字体的间隙 而生成好之后再写的话
        间隙就没法控制了
        """
        img_draw.text((i * 60 + 60, -2), tmp, get_random(), img_font)
        # 拼接随机字符串
        code += tmp
    print(code)
    # 随机验证码在登陆的视图函数里面需要用到 要比对 所以要找地方存起来并且其他视图函数也能拿到
    request.session['code'] = code
    io_obj = BytesIO()
    img_obj.save(io_obj, 'png')
    return HttpResponse(io_obj.getvalue())


def login(request):
    if request.method == 'POST':
        # 接收前端传来的数据
        username = request.POST.get('username')
        password = request.POST.get('password')
        code = request.POST.get('code')

        # 给前端返回的数据
        back_dic = {'code': 200, 'msg': '登录成功,3秒之后自动跳转', 'data': []}

        # 验证参数
        if code.upper() != request.session.get('code').upper():
            back_dic['code'] = 1002
            back_dic['msg'] = '验证码输入错误,请重新核对后输入!'
            return JsonResponse(back_dic)

        # 具体的业务逻辑
        new_pwd = get_md5(password)
        print(new_pwd)
        res = models.UserInfo.objects.filter(username=username, password=new_pwd).first()

        if not res:
            back_dic['code'] = 1003
            back_dic['msg'] = '用户名或者密码不正确!'
            return JsonResponse(back_dic)

        request.session['username'] = res.username
        request.session['id'] = res.pk
        # request.session['avatar'] = res.avatar
        return JsonResponse(back_dic)
    return render(request, 'login.html')


def register(request):
    # auth_user表扩展之后auth模块依然是可以使用的,我不给你使用auth模块了,但是这里是可以使用auth模块的
    # 如果使用了auth模块,太简单了,你实现两种方案
    if request.method == 'POST':
        # 1. 先设定给前端返回的json格式的数据
        back_dic = {'code': 200, 'msg': '注册成功hd', 'data': []}

        # 2. 接收前端提交过来的参数
        username = request.POST.get('username')
        password = request.POST.get('password')
        email = request.POST.get('email')
        my_file = request.FILES.get('my_file')

        # 3. 验证参数(这一步对于我们后端,是务必要做的,作为一个合格的后端开发人员,验证参数是必须的)
        # 我们只要符合我们业务逻辑的参数,其余的依赖拒绝掉
        # 验证参数的时候,尽量是先验证不合法的情况,反着来验证,整向验证参数也可以,只不过不好,会嵌套很多的else if
        if not username:
            back_dic['code'] = 1001  # 业务状态码(我们人为规定的), 响应状态码(1xx 2xx 3xx 4xx 5xx)
            back_dic['msg'] = '后端说用户名不能为空'  # 业务状态码(我们人为规定的), 响应状态码(1xx 2xx 3xx 4xx 5xx)
            return JsonResponse(back_dic)

        # 其余的参数验证...
        # if len(password) >= 6 and len(password) <=20:
        #     back_dic['code'] = 1002  # 业务状态码(我们人为规定的), 响应状态码(1xx 2xx 3xx 4xx 5xx)
        #     back_dic['msg'] = '密码格式不正确,请修正'  # 业务状态码(我们人为规定的), 响应状态码(1xx 2xx 3xx 4xx 5xx)
        #     return JsonResponse(back_dic)

        # 4. 开始做具体的业务逻辑
        data_dic = {}
        if my_file:
            data_dic['avatar'] = my_file
            '''
            图片被存在avatar下面是数据库中定义好的,FileField
            avatar = models.FileField(upload_to='avatar/', default='avatar/default.png')
            '''
        data_dic['username'] = username
        data_dic['email'] = email
        new_pwd = get_md5(password)
        data_dic['password'] = new_pwd
        # 把数据入库
        models.UserInfo.objects.create(**data_dic)
        return JsonResponse(back_dic)

    return render(request, 'register.html')


def set_password(request):
    if request.method == 'POST':
        # 1. 先设定给前端返回的json格式的数据
        back_dic = {'code': 200, 'msg': '密码修改成功,请使用新密码登录', 'data': []}
        # 2. 接收前端提交过来的参数
        old_password = request.POST.get('old_password')
        new_password = request.POST.get('new_password')
        re_password = request.POST.get('re_password')

        # 3. 验证参数
        if new_password != re_password:
            back_dic['code'] = 1004
            back_dic['msg'] = '两次密码输入不一致'
            return JsonResponse(back_dic)

        # 验证老密码是否正确
        old_pwd = get_md5(old_password)
        res = models.UserInfo.objects.filter(username=request.session.get('username'), password=old_pwd).first()
        if not res:
            back_dic['code'] = 1005
            back_dic['msg'] = '原密码不正确'
            return JsonResponse(back_dic)

        # 修改新密码
        new_pwd = get_md5(new_password)
        models.UserInfo.objects.filter(pk=request.session.get('id')).update(password=new_pwd)
        return JsonResponse(back_dic)


def logout(request):
    request.session.flush()
    return redirect('/login/')


def site(request, username, **kwargs):  # 有名分组,浏览器url中访问用户站点页
    # 拿到站点名字做判断,查到继续,查不到就404页面
    user_obj = models.UserInfo.objects.filter(username=username).first()
    if not user_obj:
        return render(request, '404.html')

    # 获取用户头像地址
    user = request.session.get('username')
    try:
        user_info_obj = models.UserInfo.objects.get(username=user)
        avatar = user_info_obj.avatar
    except models.UserInfo.DoesNotExist:
        avatar = '/static/img/default.jpg'  # 默认头像路径
    # 有用户名的情况下,通过用户查站点
    blog = user_obj.blog
    # 查询用户自己的所有文章
    article_list = models.Article.objects.filter(blog=blog).all()
    print('kwargs:', kwargs)
    if kwargs:
        condition = kwargs.get('condition')
        param = kwargs.get('param')
        if condition == 'category':
            #  按照分类进行搜索
            article_list = article_list.filter(category__pk=param)
        elif condition == 'tag':
            article_list = article_list.filter(tags__pk=param)  # param是url中传的pk
            # 这里的category和tags都是文章的外键字段,通过外键跳表查
        else:
            # 按照日期来搜索
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    # 查询当前站点下所有文章的分类
    cate_list = models.Category.objects.filter(blog=blog).annotate(count_num=Count("article__pk")).values_list('name',
                                                                                                               'count_num',
                                                                                                               'pk')
    # print(cate_list.query) 打印出查询语句
    # 查询出标签列表,当前站点下所有文章的标签
    tag_list = models.Tag.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('name',
                                                                                                         'count_num',
                                                                                                         'pk')
    print(tag_list)
    # <QuerySet [('python自动化', 0, 4), ('nginx', 0, 5), ('docker', 0, 6)]>
    '''
    站点和tag的关系:一个站点有多个tag,前期规划为一对多关系,外键blog在tag表中。
    标签和文章的关系,多对多,半自动化建表,外键在文章中
    根据blog站点进行过滤,分组查询结合聚合函数使用,以标签分组,统计该标签下文章的数量
    '''
    # 查询时间归档
    date_list = models.Article.objects.annotate(month=TruncMonth('create_time')).values('month').filter(
        blog=blog).annotate(c=Count('pk')).values('month', 'c')

    return render(request, 'site.html', locals())


def article_detail(request, username, article_id):
    # 获取用户头像
    user = request.session.get('username')
    # print(user)
    try:
        user_info_obj = models.UserInfo.objects.get(username=user)
        avatar = user_info_obj.avatar
    except models.UserInfo.DoesNotExist:
        avatar = '/static/img/default.jpg'  # 默认头像路径
    # 获取一个用户对象
    user_obj = models.UserInfo.objects.filter(username=username).first()
    if not user_obj:
        #  返回404页面
        return render(request, '404.html')

    blog = user_obj.blog  # 查询到的一条用户记录,点自己的blog的站点字段
    article_detail = models.Article.objects.filter(pk=article_id).first()
    # 查询当前站点下所有文章的分类
    cate_list = models.Category.objects.filter(blog=blog).annotate(count_num=Count("article__pk")).values_list('name',
                                                                                                               'count_num',
                                                                                                               'pk')
    # 查询当前站点下所有文章的tag分类
    tag_list = models.Tag.objects.filter(blog=blog).annotate(count_num=Count("article__pk")).values_list('name',
                                                                                                         'count_num',
                                                                                                         'pk')
    date_list = models.Article.objects.annotate(month=TruncMonth('create_time')).values('month').filter(
        blog=blog).annotate(c=Count("pk")).values('month', 'c')

    #  查询所有的评论列表,查询的是当前这篇文章的所有评论列表
    comment_list = models.Comment.objects.filter(article_id=article_id).all()

    from utils.mypage1 import Pagination
    current_page = request.GET.get('page')
    try:
        current_page = int(current_page)
    except Exception as e:
        current_page = 1
    # # 文章详情页给评论区做一个分页
    page_obj = Pagination(current_page, comment_list.count(), per_page_num=5)  # 实例化
    comment_list = comment_list[page_obj.start:page_obj.end]

    return render(request, 'article_detail.html', locals())


from django.db.models import F


def up_and_down(request):
    if request.method == 'POST':
        # 先获取前端传过来的数据
        back_dict = {'code': 200, 'msg': '支持成功!'}
        is_up = request.POST.get('is_up')
        print(is_up)
        is_up = json.loads(is_up)
        article_id = request.POST.get('article_id')
        # 判断用户是否登录
        if not request.session.get('username'):
            back_dict['code'] = 1006
            back_dict['msg'] = '<a href="/login/" style="color:red">请先登录</a>'
            return JsonResponse(back_dict)
        # 判断是否点赞过
        res = models.UpAndDown.objects.filter(article_id=article_id, user_id=request.session.get('id')).first()
        if res:
            back_dict['code'] = 1007
            back_dict['msg'] = '你已经支持过了!'
            return JsonResponse(back_dict)
        # 处理正常的业务逻辑
        # 操作up_and_down、 article。前端点赞和点踩都是绑定的同一个点击事件。当点击事件被触发,is_up没有值的话,那就是is_down触发了.
        if is_up:
            # 在文章表中增加数量
            models.Article.objects.filter(pk=article_id).update(up_num=F('up_num') + 1)
            back_dict['msg'] = '支持成功!'
        else:
            models.Article.objects.filter(pk=article_id).update(down_num=F('down_num') + 1)
            back_dict['msg'] = '加油!'
        models.UpAndDown.objects.create(is_up=is_up, article_id=article_id, user_id=request.session.get('id'))
        return JsonResponse(back_dict)


def comment(request):
    '''
    1、 登录才能评论
    2、 评论需要入库
        文章表和评论表
    '''
    if request.method == 'POST':
        back_dic = {'code': 200, 'msg': '评论成功'}
        # 接收参数
        article_id = request.POST.get('article_id')
        content = request.POST.get('content')
        parent_id = request.POST.get('parent_id')
        print(article_id, content, parent_id)
        # 判断用户是否登录
        if not request.session.get('username'):
            back_dic['code'] = 1008
            back_dic['msg'] = '请先登录之后再评论'
            return JsonResponse(back_dic)
        # 加一个事务
        # transaction.atomic() 装饰器,确保函数中的所有数据库操作要么全部成功提交,要么全部回滚。
        from django.db import transaction
        try:
            with transaction.atomic():
                # 操作表,文章表中评论字段加1
                models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num') + 1)
                # 评论表
                models.Comment.objects.create(content=content, article_id=article_id, user_id=request.session.get('id'),
                                              parent_id=parent_id)
        except Exception as e:
            transaction.rollback()
        return JsonResponse(back_dic)


# 评论分页功能
def comment_page(request):
    # 根据当前第几页查询当前页的评论列表数据
    if request.method == 'POST':
        back_dic = {'code': 200, 'msg': '查询成功'}
        # 接受参数
        current_page = request.POST.get('current_page')
        try:
            current_page = int(current_page)
        except Exception:
            current_page = 1
        article_id = request.POST.get('article_id')
        # 验证参数:一定要验证,不要相信任何 人传来 的数据

        # 每页显示的条数
        per_page_num = 5

        start_page = (current_page - 1) * per_page_num
        end_page = current_page * per_page_num
        # 查询评论列表数据
        comment_list = models.Comment.objects.filter(article_id=article_id).all()[start_page:end_page]  # queryset对象
        comment_list_obj = []
        i = 1
        for comment in comment_list:
            comment_list_obj.append({
                'forloop': i,
                'pk': comment.pk,
                'comment_time': comment.comment_time,
                'username': comment.user.username,
                'content': comment.content
            })
            i += 1
        print(comment_list_obj)
        back_dic['data'] = comment_list_obj
        return JsonResponse(back_dic)

三、前台主页面

1、html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>主页面</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js"></script>
    <script src="/static/layer-v3.5.1/layer/layer.js"></script>
    <style>
        .bingpai {
            line-height: 50px;
        }

        .bingpai img {
            height: 100%;
        }

        .s1 {
            margin-right: 10px;
        }
    </style>
    {% block css %}

    {% endblock %}
</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="/home/">首页<span class="sr-only">(current)</span></a></li>
                <li><a href="/app02/home/">后台</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="bingpai  nav navbar-nav navbar-right">
                {# 做一个判断:如果session存在那么显示头像、用户名。如果不存在,显示登录和注册#}
                {% if request.session.username %}
                    <li><img style="border-radius: 50%" src="/media/{{ avatar }}" id="my_img" width="25px" alt=""></li>
                    <li><a href="/{{ request.session.username }}/">{{ request.session.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="#">修改头像</a></li>
                            <li><a href="/logout/">退出登录</a></li>
                            <li><a href="#">后台系统</a></li>
                        </ul>
                    </li>
                {% else %}
                    <li><a href="/register/">注册</a></li>
                    <li><a href="/login/">登录</a></li>
                {% endif %}
            </ul>
            <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">
                        <div class="row">
                            <h1 class="text-center">修改密码</h1>
                            <div class="col-md-8 col-md-offset-2">
                                <div class="form-group">
                                    用户名: <input type="text" class="form-control" readonly id="username"
                                                   value="{{ request.session.username }}">
                                </div>
                                <div class="form-group">
                                    原密码: <input type="password" class="form-control" id="old_password">
                                </div>
                                <div class="form-group">
                                    新密码: <input type="password" class="form-control" id="new_password">
                                </div>
                                <div class="form-group">
                                    确认密码: <input type="password" class="form-control" id="re_password">
                                </div>
                                <div class="form-group">
                                    <input type="button" value="修改密码" class="btn  btn-success btn-block btn_password">
                                </div>
                            </div>

                        </div>
                    </div>
                </div>
            </div>
        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>
<div class="container-fluid">
    <div class="row">
        {% block content %}
            <div class="col-md-2">
                {#列表组占位#}
                <div class="list-group">
                    <a href="#" class="list-group-item active">
                        分类
                    </a>
                    <a href="#" class="list-group-item">精华</a>
                    <a href="#" class="list-group-item">候选</a>
                    <a href="#" class="list-group-item">订阅</a>
                    <a href="#" class="list-group-item">关注</a>
                </div>
            </div>
            <div class="col-md-8">
                <ul class="media-list">
                    {% for article in article_list %}
                        <li class="media">
                            <h4 class="media-heading"><a href="{{ article.blog.userinfo.username }}/{{ article.id }}">{{ article.title }}</a></h4>
                            <div class="media-left">
                                <a href="#">
                                    <img class="media-object" style="height: 80px;
    border-radius: 50%;" src="/media/{{ article.blog.userinfo.avatar }}" width="80" alt="...">
                                </a>
                            </div>
                            <div class="media-body">
                                {{ article.desc }}
                            </div>
                            <div style="    margin-top: 15px;">
                                <span class="s1"><a href="">{{ article.blog.userinfo.username }}</a></span>
                                <span class="s1">{{ article.create_time|date:'Y-m-d' }}</span>
                                <span class="s1"><span class="glyphicon glyphicon-thumbs-up"></span>{{ article.up_num }}</span>
                                <span class="s1"><span
                                        class="glyphicon glyphicon-thumbs-down"></span>{{ article.down_num }}</span>
                                <span class="s1"><span
                                        class="glyphicon glyphicon-comment"></span>{{ article.comment_num }}</span>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>
            <div class="col-md-2">
                <div class="list-group">
                    <a href="#" class="list-group-item active">
                        热点新闻
                    </a>
                    <a href="#" class="list-group-item">编辑推荐</a>
                    <a href="#" class="list-group-item">48小时评论排行榜</a>
                    <a href="#" class="list-group-item">10天推荐排行榜</a>
                    <a href="#" class="list-group-item">48小时阅读排行</a>
                </div>
            </div>
        {% endblock %}
    </div>
</div>

<script>
    $('.btn_password').click(function () {
        var old_password = $("#old_password").val();
        var new_password = $("#new_password").val();
        var re_password = $("#re_password").val();

        // 参数验证
        // re: match  search findall ...

        // 3. 发起ajax请求
        $.ajax({
            url: '/set_password/',
            type: 'post',
            data: {
                old_password: old_password,
                new_password: new_password,
                re_password: re_password,
                csrfmiddlewaretoken: '{{ csrf_token }}'
            },
            success: function (res) {
                if (res.code === 200) {
                    layer.msg(res.msg, {}, function () {
                        location.reload();
                    })
                } else {
                    layer.msg(res.msg, {});
                }
            }
        })
    })
</script>
{% block js %}

{% endblock %}
</body>
</html>

2、views

# @login_required(login_url='/login/')
def home(request):
    article_list = models.Article.objects.all()
    user = request.session.get('username')
    # print(user)
    try:
        user_info_obj = models.UserInfo.objects.get(username=user)
        avatar = user_info_obj.avatar
        # print(avatar)
    except models.UserInfo.DoesNotExist:
        avatar = '/static/img/default.jpg'  # 默认头像路径

    return render(request, 'home.html', locals())

3、前台主页面

四、注册和登录功能

1、注册html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册页面</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js"></script>
    <script src="/static/layer-v3.5.1/layer/layer.js"></script>
    <style>
        /* 自定义背景图片样式 */
        .container {
            background-image: url('/static/img/雪山.jpeg'); /* 背景图片的URL */
            background-size: cover; /* 图片将被缩放以填充整个容器 */
            background-repeat: no-repeat; /* 不重复背景图片 */
            background-position: center center; /* 图片在容器中居中 */
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h1 class="text-center">注册页面</h1>
            <div class="form-group">
                <label for="username">用户名:</label>
                <input type="text" id="username" placeholder="username" class="form-control" msg="用户名必须填写">
            </div>
            <div class="form-group">
                <label for="password">密码:</label>
                <input type="password" id="password" placeholder="password" class="form-control" msg="密码必须填写">
            </div>
            <div class="form-group">
                <label for="re_password">确认密码:</label>
                <input type="password" id="re_password" placeholder="re_password" class="form-control"
                       msg="确认密码必须填写">
            </div>
            <div class="form-group">
                <label for="email">邮箱:</label>
                <input type="text" id="email" placeholder="email" class="form-control">
            </div>
            <div class="form-group">
                <label for="my_file">上传头像:
                    <img src="/static/img/default.jpg" id="my_img" width="110" alt="">
                </label>
                <input type="file" id="my_file" style="display: none" class="form-control">
            </div>
            <div class="form-group">
                <input type="button" value="提交" class="btn btn-primary btn-block">
            </div>
        </div>
    </div>
</div>

<script>
    // change事件当图片更换之后触发执行
    $("#my_file").change(function () {
        // 借助于js的文件阅读器
        var myFileReadObj = new FileReader()
        // 获取文件数据
        var myFileDataObj = $(this)[0].files[0];
        //把文件数据提交给阅读器来获取图片数据
        myFileReadObj.readAsDataURL(myFileDataObj); // 这里是移步提交
        // 把读取到的图片放到img src中
        myFileReadObj.onload = function () {
            $("#my_img").attr('src', myFileReadObj.result)
        };
    })

    $(".btn").click(function () {
        // 1 获取表单数据
        let username = $("#username").val();
        let password = $("#password").val();
        let re_password = $("#re_password").val();
        let email = $("#email").val();
        //  获取文件数据
        let my_file = $("#my_file")[0].files[0];

        // 2 参数验证
        let fields = [
            {id: 'username', msg: '用户名必须填写'},
            {id: 'password', msg: '密码必须填写'},
            {id: 're_password', msg: '确认密码必须填写'}
        ];
        for (let i = 0; i < fields.length; i++) {
            if (!$("#" + fields[i].id).val()) {
                layer.msg(fields[i].msg);
                return;
            }
        }

        var myFormDataObj = new FormData;
        myFormDataObj.append('username', username)
        myFormDataObj.append('password', password)
        myFormDataObj.append('re_password', re_password)
        myFormDataObj.append('email', email)
        myFormDataObj.append('my_file', my_file)
        myFormDataObj.append('csrfmiddlewaretoken', '{{ csrf_token }}')
        console.log(myFormDataObj.get('email'));

        // 3 发起ajax请求
        $.ajax({
            url: '',
            type: 'POST',
            data: myFormDataObj,
            contentType: false,
            processData: false,
            success: function (res) {
                if (res.code === 200) {
                    layer.msg(res.msg, {}, function () {
                        location.href = '/login/'
                    })
                }
            }
        })
    })
</script>

</body>
</html>

2、注册views

def register(request):
    # auth_user表扩展之后auth模块依然是可以使用的,我不给你使用auth模块了,但是这里是可以使用auth模块的
    # 如果使用了auth模块,太简单了,你实现两种方案
    if request.method == 'POST':
        # 1. 先设定给前端返回的json格式的数据
        back_dic = {'code': 200, 'msg': '注册成功hd', 'data': []}

        # 2. 接收前端提交过来的参数
        username = request.POST.get('username')
        password = request.POST.get('password')
        email = request.POST.get('email')
        my_file = request.FILES.get('my_file')

        # 3. 验证参数(这一步对于我们后端,是务必要做的,作为一个合格的后端开发人员,验证参数是必须的)
        # 我们只要符合我们业务逻辑的参数,其余的依赖拒绝掉
        # 验证参数的时候,尽量是先验证不合法的情况,反着来验证,整向验证参数也可以,只不过不好,会嵌套很多的else if
        if not username:
            back_dic['code'] = 1001  # 业务状态码(我们人为规定的), 响应状态码(1xx 2xx 3xx 4xx 5xx)
            back_dic['msg'] = '后端说用户名不能为空'  # 业务状态码(我们人为规定的), 响应状态码(1xx 2xx 3xx 4xx 5xx)
            return JsonResponse(back_dic)

        # 其余的参数验证...
        # if len(password) >= 6 and len(password) <=20:
        #     back_dic['code'] = 1002  # 业务状态码(我们人为规定的), 响应状态码(1xx 2xx 3xx 4xx 5xx)
        #     back_dic['msg'] = '密码格式不正确,请修正'  # 业务状态码(我们人为规定的), 响应状态码(1xx 2xx 3xx 4xx 5xx)
        #     return JsonResponse(back_dic)

        # 4. 开始做具体的业务逻辑
        data_dic = {}
        if my_file:
            data_dic['avatar'] = my_file
            '''
            图片被存在avatar下面是数据库中定义好的,FileField
            avatar = models.FileField(upload_to='avatar/', default='avatar/default.png')
            '''
        data_dic['username'] = username
        data_dic['email'] = email
        new_pwd = get_md5(password)
        data_dic['password'] = new_pwd
        # 把数据入库
        models.UserInfo.objects.create(**data_dic)
        return JsonResponse(back_dic)

    return render(request, 'register.html')

3、登录 html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>主页面</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js"></script>
    <script src="/static/layer-v3.5.1/layer/layer.js"></script>
    <style>
        /* 自定义背景图片样式 */
        .container {
            background-image: url('/static/img/雪山.jpeg'); /* 背景图片的URL */
            background-size: cover; /* 图片将被缩放以填充整个容器 */
            background-repeat: no-repeat; /* 不重复背景图片 */
            background-position: center center; /* 图片在容器中居中 */
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h1 class="text-center">登录页面</h1>
            <div class="form-group">
                <label for="username">用户名:</label>
                <input type="text" id="username" class="form-control" msg="用户名必须填写">
            </div>
            <div class="form-group">
                <label for="username">密码:</label>
                <input type="text" id="password" class="form-control" msg="密码必须填写">
            </div>

            <div class="form-group">
                <label for="code">验证码:</label>
                <div class="row">
                    <div class="col-md-6">
                        <input type="text" id="code" class="form-control">
                    </div>
                    <div class="col-md-6">
                        <img src="/get_code/" style="width: 360px;
    height: 35px;" alt="">
                    </div>
                </div>

            </div>
            <div class="form-group">
                <input type="button" value="提交" class="btn btn-primary btn-block">
            </div>
        </div>
    </div>
</div>

<script>
    $(".btn").click(function () {
        // 1. 获取表单数据
        var username = $("#username").val();  // undefined
        var password = $("#password").val();
        var code = $("#code").val();

        // 2. 参数验证
        // ...
        if (code.length != 5) {
            layer.msg('验证码输入不正确');
            return
        }
        // 3. 发起ajax请求
        $.ajax({
            url: '',
            type: 'post',
            data: {username:username, password:password, code:code, csrfmiddlewaretoken:'{{ csrf_token }}'},
            success: function (res) {
                if (res.code == 200) {
                    layer.msg(res.msg, {}, function () {
                        location.href = '/home/'
                    })
                } else {
                    layer.msg(res.msg, {});
                }
            }
        })

    });

</script>
</body>
</html>

4、登录 views

def login(request):
    if request.method == 'POST':
        # 接收前端传来的数据
        username = request.POST.get('username')
        password = request.POST.get('password')
        code = request.POST.get('code')

        # 给前端返回的数据
        back_dic = {'code': 200, 'msg': '登录成功,3秒之后自动跳转', 'data': []}

        # 验证参数
        if code.upper() != request.session.get('code').upper():
            back_dic['code'] = 1002
            back_dic['msg'] = '验证码输入错误,请重新核对后输入!'
            return JsonResponse(back_dic)

        # 具体的业务逻辑
        new_pwd = get_md5(password)
        print(new_pwd)
        res = models.UserInfo.objects.filter(username=username, password=new_pwd).first()

        if not res:
            back_dic['code'] = 1003
            back_dic['msg'] = '用户名或者密码不正确!'
            return JsonResponse(back_dic)

        request.session['username'] = res.username
        request.session['id'] = res.pk
        # request.session['avatar'] = res.avatar
        return JsonResponse(back_dic)
    return render(request, 'login.html')

5、登录和注册页面

 

五、个人站点页

1、html

{% extends 'home.html' %}

{% block css %}
    <style>
        /* 去掉下划线 */
        a {
            text-decoration: none;
        }

        /* 鼠标移动到上面变色 */
        a:hover {
            color: red; /* 或者您想要的颜色 */
        }

        .s1 {
            margin-right: 10px;
            color: #999;
        }

        .content {
            font-size: 16px;
            color: #444;
        }

        .clearfix:focus {
            content: '';
            display: block;
            clear: both;
        }
    </style>
{% endblock %}
{% block content %}
    <div class="col-md-3">
        <div class="panel panel-info">
            <div class="panel-heading">文章分类</div>
            <div class="panel-body">
                {% for cate in cate_list %}
                    <p><a href="/{{ username }}/category/{{ cate.2 }}">{{ cate.0 }}({{ cate.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-danger">
            <div class="panel-heading">日期归档</div>
            <div class="panel-body">
                {% for date in date_list %}
                    <p>
                        <a href="/{{ username }}/archive/{{ date.month|date:'Y-m' }}">{{ date.month|date:'Y年m月' }}({{ date.c }})</a>
                    </p>
                {% endfor %}
            </div>
        </div>
    </div>
    <div class="col-md-9">
        {% for article in article_list %}
            <div style="margin-top: 20px;">
                <h3><a href="/{{ username }}/{{ article.id }}">{{ article.title }}</a></h3>
                <div class="content">
                    {{ article.desc }}
                </div>
                <div class="clearfix">
                    <div style="margin-top: 10px;" class="pull-right ">
                        <span class="s1">posted</span>
                        <span class="s1">@</span>
                        <span class="s1">{{ article.create_time|date:'Y-m-d' }}</span>
                        <span class="s1">{{ article.blog.userinfo.username }}</span>
                        <span class="s1">点赞({{ article.up_num }})</span>
                        <span class="s1">点踩({{ article.down_num }})</span>
                        <span class="s1">评论({{ article.comment_num }})</span>
                    </div>
                </div>
            </div>

        {% endfor %}
    </div>
{% endblock %}

2、views

def site(request, username, **kwargs):  # 有名分组,浏览器url中访问用户站点页
    # 拿到站点名字做判断,查到继续,查不到就404页面
    user_obj = models.UserInfo.objects.filter(username=username).first()
    if not user_obj:
        return render(request, '404.html')

    # 获取用户头像地址
    user = request.session.get('username')
    try:
        user_info_obj = models.UserInfo.objects.get(username=user)
        avatar = user_info_obj.avatar
    except models.UserInfo.DoesNotExist:
        avatar = '/static/img/default.jpg'  # 默认头像路径
    # 有用户名的情况下,通过用户查站点
    blog = user_obj.blog
    # 查询用户自己的所有文章
    article_list = models.Article.objects.filter(blog=blog).all()
    print('kwargs:', kwargs)
    if kwargs:
        condition = kwargs.get('condition')
        param = kwargs.get('param')
        if condition == 'category':
            #  按照分类进行搜索
            article_list = article_list.filter(category__pk=param)
        elif condition == 'tag':
            article_list = article_list.filter(tags__pk=param)  # param是url中传的pk
            # 这里的category和tags都是文章的外键字段,通过外键跳表查
        else:
            # 按照日期来搜索
            year, month = param.split('-')
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    # 查询当前站点下所有文章的分类
    cate_list = models.Category.objects.filter(blog=blog).annotate(count_num=Count("article__pk")).values_list('name',
                                                                                                               'count_num',
                                                                                                               'pk')
    # print(cate_list.query) 打印出查询语句
    # 查询出标签列表,当前站点下所有文章的标签
    tag_list = models.Tag.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('name',
                                                                                                         'count_num',
                                                                                                         'pk')
    print(tag_list)
    # <QuerySet [('python自动化', 0, 4), ('nginx', 0, 5), ('docker', 0, 6)]>
    '''
    站点和tag的关系:一个站点有多个tag,前期规划为一对多关系,外键blog在tag表中。
    标签和文章的关系,多对多,半自动化建表,外键在文章中
    根据blog站点进行过滤,分组查询结合聚合函数使用,以标签分组,统计该标签下文章的数量
    '''
    # 查询时间归档
    date_list = models.Article.objects.annotate(month=TruncMonth('create_time')).values('month').filter(
        blog=blog).annotate(c=Count('pk')).values('month', 'c')

    return render(request, 'site.html', locals())

3、页面

六、文章详情页

1、html

{% extends 'home.html' %}
{% block css %}
    <style>
        .s1 {
            margin-right: 10px;
            color: #999;
        }

        .content {
            font-size: 16px;
            color: #444;
        }

        #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;
        }

        .clearfix:focus {
            content: '';
            display: block;
            clear: both;
        }
    </style>
{% endblock %}
{% block content %}
    <div class="col-md-3">
        <div class="panel panel-info">
            <div class="panel-heading">文章分类</div>
            <div class="panel-body">
                {% for cate in cate_list %}
                    <p><a href="/{{ username }}/category/{{ cate.2 }}">{{ cate.0 }}({{ cate.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-danger">
            <div class="panel-heading">日期归档</div>
            <div class="panel-body">
                {% for date in date_list %}
                    <p>
                        <a href="/{{ username }}/archive/{{ date.month|date:'Y-m' }}">{{ date.month|date:'Y年m月' }}({{ date.c }})</a>
                    </p>
                {% endfor %}
            </div>
        </div>
    </div>
    <div class="col-md-9">
        <h3 style="color: #9cba39">{{ article_detail.title }}</h3>
        <div class="content">
            {{ article_detail.content|safe }}
        </div>
        {#    点赞点踩样式开始#}
        <div class="clearfix">
            <div id="div_digg">
                <div class="diggit active" onclick="votePost(17636820,'Digg')">
                    <span class="diggnum" id="digg_count">{{ article_detail.up_num }}</span>
                </div>
                <div class="buryit active" onclick="votePost(17636820,'Bury')">
                    <span class="burynum" id="bury_count">{{ article_detail.down_num }}</span>
                </div>
                <div class="clear"></div>
                <div class="diggword" id="digg_tips" style="color:red;"></div>
            </div>
        </div>
        {#    点赞点踩样式结束#}

        {#        评论列表的展示开始#}
        <div class="comment_list">
            <h3><span class="glyphicon glyphicon-comment"></span>评论列表</h3>
            <ul class="list-group">
                {% for comment in comment_list %}
                    <li class="list-group-item">
                        <span style="margin-right: 10px"># {{ forloop.counter }}楼</span>
                        <span style="margin-right: 10px">{{ comment.comment_time }}</span>
                        <span style="margin-right: 10px">{{ comment.user.username }}</span>
                        <span style="margin-right: 10px" class="pull-right"><a href="javascript:;"
                                                                               comment_username="{{ comment.user.username }}"
                                                                               comment_id='{{ comment.pk }}'
                                                                               class="reply">回复</a></span>

                        <div class="content" style="margin-left: 14px">
                            {% if comment.parent %}
                                {# 如果父id存在,说明是子评论,子评论拼接一个@  #}
                                @{{ comment.content }}
                            {% else %}
                                <p> {{ comment.parent.user.username }}</p>
                                {# 这里的parent是Comment表的自关联外键  #}
                                <p>
                                    {{ comment.content }}
                                </p>
                            {% endif %}
                        </div>
                    </li>
                {% endfor %}
            </ul>


        </div>
        {#        评论列表的展示结束#}
        {#    评论功能开始#}
        {#分页开始#}
        <div class="text-center">{{ page_obj.page_html|safe }}</div>
        {#分页结束#}
        <div class="comment">
            <p>
                <span><span class="glyphicon glyphicon-comment"></span>发表评论</span>
            </p>
            <p>
                <textarea name="" id="content" cols="30" rows="10"></textarea>
            </p>
            <p>
                <button class="btn btn-success comment_content">提交评论</button>
            </p>
        </div>
        {#    评论功能结束#}
        <div style="height: 500px"></div>
    </div>
{% endblock %}

{% block js %}
    <script>
        {% comment %} function votePost(id, flag) {
             var is_up = flag == 'Digg' ? 0 :1;
         }{% endcomment %}
        $(".active").click(function () {
            // 区分点得是赞还是踩?
            var is_up = $(this).hasClass('diggit');
            var _this = $(this);
            console.log(is_up)
            // 文章id
            var article_id = '{{ article_detail.pk }}';

            // 发起ajax请求
            $.ajax({
                url: '/up_and_down/',
                type: 'post',
                data: {is_up: is_up, article_id: article_id, csrfmiddlewaretoken: '{{ csrf_token }}'},
                success: function (res) {
                    if (res.code === 200) {
                        $("#digg_tips").text(res.msg);
                        // 如果是点赞,就让点赞数+1,如果是点踩,就让点踩数+1
                        {#var old_num = parseInt(_this.children().text()); // 隐藏的bug#}
                        var old_num = Number(_this.children().text()); // 隐藏的bug
                        _this.children().text(old_num + 1)
                    } else {
                        $("#digg_tips").html(res.msg);
                    }
                }

            })
        })

        var parent_id = null;
        // 评论功能开始
        $(".comment_content").click(function () {
            // 1. 获取参数
            var article_id = '{{ article_detail.pk }}';

            // 2. 获取评论的内容
            var content = $('#content').val();

            if (parent_id) {
                // 子评论, 把评论的内容截取掉
                var aa = content.indexOf('\n')  // 如果匹配到了,就返回它在字符串中的位置
                content = content.slice();
            }
            $.ajax({
                url: '/comment/',
                type: 'post',
                data: {
                    article_id: article_id,
                    content: content,
                    parent_id: parent_id,
                    csrfmiddlewaretoken: '{{ csrf_token }}'
                },
                success: function (res) {
                    // 做评论内容临时渲染  es6语法中的模板语法
                    var username = '{{ request.session.username }}';
                    var html = `  <li class="list-group-item">
                        <span style="margin-right: 10px"><span class="glyphicon glyphicon-comment"></span>${username}</span>
                        <div class="content" style="margin-left: 14px">
                            ${content}
                        </div>
                    </li>`
                    $(".list-group").append(html);
                    $("#content").val('');
                }
            })

        })

        // 子评论功能开始
        $(".reply").click(function () {
            var comment_username = $(this).attr('comment_username');
            parent_id = $(this).attr('comment_id');
            $("#content").val(comment_username + '\n').focus();
        })

        // 评论页表分页功能
        //上面导入了页面,所以能导入页面 article_detail.content|safe
        $(".btn_page").click(function () {
            // 做分页
            // 1、当前页是第几页
            var current_page = $(this).attr('current_page');
            var article_id = '{{ article_detail.pk }}'

            // 高亮显示,先删除前一个高亮,再给当前的页面高亮。class='active'表示的是高亮效果
            $('.active').removeClass('active');
            $(this).parent().addClass('active');

            // 2、 发起ajax请求
            $.ajax({
                url: '/comment_page/',
                type: 'post',
                data: {current_page: current_page, article_id: article_id, csrfmiddlewaretoken: '{{ csrf_token }}'},
                success: function (res) {
                    console.log(res);
                    if (res.code === 200) {
                        var html = '';
                        // index是data数据里面的索引, obj是data数据,这里是传来一个字典{forloop: 1, pk: 47, comment_time: '2023-08-21T16:33:00.470', username: 'jingzhiz', content: '论'}
                        $.each(res.data, function (index, obj) {
                            html += `<li class="list-group-item">
                        {#            #1楼 2023-08-17 11:30 程序员李老师#}
                        <span style="margin-right: 10px"># ${obj.forloop}楼</span>
                        <span style="margin-right: 10px">${obj.comment_time}</span>
                        <span style="margin-right: 10px">${obj.username}</span>
                        <span style="margin-right: 10px" class="pull-right"><a href="javascript:;"
                                                                               comment_username="${obj.username}"
                                                                               comment_id='${obj.pk}'
                                                                               class="reply">回复</a></span>

                        <div class="content" style="margin-left: 14px">
                           ${obj.content}
                        </div>
                    </li>`
                        });
                        $('.list-group').html(html);
                    }

                }
            })
        })

    </script>
{% endblock %}

2、views

def article_detail(request, username, article_id):
    # 获取用户头像
    user = request.session.get('username')
    # print(user)
    try:
        user_info_obj = models.UserInfo.objects.get(username=user)
        avatar = user_info_obj.avatar
    except models.UserInfo.DoesNotExist:
        avatar = '/static/img/default.jpg'  # 默认头像路径
    # 获取一个用户对象
    user_obj = models.UserInfo.objects.filter(username=username).first()
    if not user_obj:
        #  返回404页面
        return render(request, '404.html')

    blog = user_obj.blog  # 查询到的一条用户记录,点自己的blog的站点字段
    article_detail = models.Article.objects.filter(pk=article_id).first()
    # 查询当前站点下所有文章的分类
    cate_list = models.Category.objects.filter(blog=blog).annotate(count_num=Count("article__pk")).values_list('name',
                                                                                                               'count_num',
                                                                                                               'pk')
    # 查询当前站点下所有文章的tag分类
    tag_list = models.Tag.objects.filter(blog=blog).annotate(count_num=Count("article__pk")).values_list('name',
                                                                                                         'count_num',
                                                                                                         'pk')
    date_list = models.Article.objects.annotate(month=TruncMonth('create_time')).values('month').filter(
        blog=blog).annotate(c=Count("pk")).values('month', 'c')

    #  查询所有的评论列表,查询的是当前这篇文章的所有评论列表
    comment_list = models.Comment.objects.filter(article_id=article_id).all()

    from utils.mypage1 import Pagination
    current_page = request.GET.get('page')
    try:
        current_page = int(current_page)
    except Exception as e:
        current_page = 1
    # # 文章详情页给评论区做一个分页
    page_obj = Pagination(current_page, comment_list.count(), per_page_num=5)  # 实例化
    comment_list = comment_list[page_obj.start:page_obj.end]

    return render(request, 'article_detail.html', locals())

3、文章详情页

七、后端主页

1、app02 views

from django.shortcuts import render, redirect
from app01 import models
from django.http import JsonResponse


# Create your views here.
def home(request):
    # 获取用户头像地址
    avatar = models.UserInfo.objects.get(username=request.session.get('username')).avatar
    print(avatar)
    return render(request, 'backend/home.html', locals())


def article_list(request):
    article_list = models.Article.objects.all()
    return render(request, 'backend/article_list.html', locals())


# 后台系统添加文章
def add_article(request):
    user_obj = models.UserInfo.objects.filter(pk=request.session.get('id')).first()
    if not user_obj:
        return redirect('/login/')
    blog = user_obj.blog

    # 查询所有文章的分类列表
    cate_list = models.Category.objects.all()

    # 查询所有的标签列表
    tags_list = models.Tag.objects.all()

    # 查询标签列表
    if request.method == 'POST':
        # 定义返回给前端的json数据格式
        back_dict = {'code': 200, 'msg': '添加成功,3秒后自动跳转页面'}

        # 接收前端传递过来的参数
        title = request.POST.get('title')
        cate_id = request.POST.get('cate_id')
        content = request.POST.get('content')
        tags = request.POST.get('tags')
        # print(tags, type(tags))  # 2,4 <class 'str'>
        # 需要将字符串转为列表使用
        tags_list = tags.split(',')  # [2,4]

        # 验证参数
        if not title:
            back_dict['code'] = 6001
            back_dict['msg'] = '标题不能为空'
            return JsonResponse(back_dict)
        if not cate_id:
            back_dict['code'] = 6002
            back_dict['msg'] = '分类必须要选'
            return JsonResponse(back_dict)
        if not cate_id:
            back_dict['code'] = 6003
            back_dict['msg'] = '标签必须要选'
            return JsonResponse(back_dict)

        '''
        1.摘要截取的问题
        2.xss攻击的问题:
            原理:有了script标签,把提交过来的内容过滤出script标签,然后做删除
            解决方法:使用正则匹配script,匹配到之后,做删除
            或者利用第三方模块来处理: bs4模块
            pip install beautifulsoup4
            BeautifulSoup它是在爬虫里面,他能够筛选数据,清洗数据,html数据
            BeautifulSoup(数据,解析器)
        '''
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(content, 'html.parser')
        # print(soup.find_all('a'))
        for tag in soup.find_all():
            # 获取所有的标签
            # print(tag.name)

            # 如果标签是script则使用decompose()方法删除
            if tag.name == 'script':
                tag.decompose()

        # 数据入库
        # 操作文章表、文章和标签的第三张表

        # 文章摘要
        # print(soup.text)  # 文本
        # print(soup.contents)  # 二进制的
        desc = soup.text[0: 100]

        # create的结果就是新增的 obj 对象
        article_obj = models.Article.objects.create(title=title, content=str(soup),
                                                    desc=desc, category_id=cate_id, blog=blog)
        # 操作第三张表,批量操作数据bulk_create
        articletag2obj_list = []
        for i in tags_list:
            article2tag_obj = models.Article2Tag(article_id=article_obj.pk, tag_id=i)
            articletag2obj_list.append(article2tag_obj)
        models.Article2Tag.objects.bulk_create(articletag2obj_list)

        return JsonResponse(back_dict)

    return render(request, 'backend/add_article.html', locals())


# 上传图片
def upload_image(request):
    if request.method == 'POST':
        file_obj = request.FILES.get('imgFile')
        # print(file_obj)  # <MultiValueDict: {'imgFile': [<InMemoryUploadedFile: 7.jpg (image/jpeg)>]}>

        # 拼接上传文件的路径
        import os
        from django.conf import settings

        # /media/article_img/xx.123.png
        BASE_DIE = os.path.join(settings.BASE_DIR, 'media', 'article_img')

        # 生成新的文件名
        import uuid
        new_str = str(uuid.uuid4())
        new_uuid = new_str.replace('-', '')
        new_file_name = new_uuid + '.' + file_obj.name.rsplit('.')[-1]

        # file_name = os.path.join(BASE_DIE, file_obj.name)
        new_file = os.path.join(BASE_DIE, new_file_name)

        with open(new_file, 'wb') as f:
            # with open(file_name, 'wb') as f:
            for line in file_obj:
                f.write(line)

    # return HttpResponse('OK')
    return JsonResponse({
        "error": 0,
        # "url": "/media/article_img/%s" % file_obj.name
        "url": "/media/article_img/%s" % new_file_name
    })


# 后台文章删除
def del_article(request):
    article_id = request.GET.get('id')
    # 删除文章(级连更新级连删除)
    models.Article.objects.filter(id=article_id).delete()

    # article2tag_obj = models.Article2Tag.objects.filter(article_id=article_id).first()
    # if article2tag_obj:
    #     id = article2tag_obj.pk
    #     # 删除tag和article第三张表记录
    #     models.Article2Tag.objects.filter(article_id=id).delete()

    return redirect('article_list')  # 这里用的是路由的反向解析
    # return redirect('app02/article')  # 有多个app应用,重定向先从外面app进来,不直接走templates


# 后台文章修改
def update_article(request):
    # 获取前端GET方式传来的id
    article_id = request.GET.get('id')
    # 用于前端展示原来的数据
    article = models.Article.objects.filter(pk=article_id).first()

    # 接收ajax传来数据
    if request.method == 'POST':
        title = request.POST.get('title')
        up_num = request.POST.get('up_num')
        down_num = request.POST.get('down_num')
        comment_num = request.POST.get('comment')

        # 更新数据库
        models.Article.objects.filter(pk=article_id).update(title=title, up_num=up_num, down_num=down_num, comment_num=comment_num)

        # 给前端返回提示语
        back_dict = {'code': 200, 'msg': '修改成功'}

        return JsonResponse(back_dict)

    return render(request, 'backend/update_article.html', locals())


# 分类列表
def cate_article(request):
    cate_article = models.Category.objects.all()
    return render(request, 'backend/cate_article.html', locals())


# 标签列表
def tags_article(request):
    tags_article = models.Tag.objects.all()
    return render(request, 'backend/tags_article.html', locals())

2、总路由 urls

from django.contrib import admin
from django.urls import path, re_path, include
from django.views.static import serve
from django.conf import settings
from app01 import views

urlpatterns = [
    # 路由分发
    re_path('^app02/', include('app02.urls')),
    path('admin/', admin.site.urls),
    path('comment_page/', views.comment_page),
    path('home/', views.home),  # app01 的home
    path('register/', views.register),
    path('login/', views.login),
    path('logout/', views.logout),
    path('set_password/', views.set_password),
    path('get_code/', views.get_code),  # 验证码
    path('up_and_down/', views.up_and_down),  # 点赞和点踩
    path('comment/', views.comment),
    re_path('media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT}),  # 开放media文件夹
    re_path('(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)', views.site),
    # <p><a href="/{{ username }}/tag/{{ tag.2 }}">{{ tag.0 }}({{ tag.1 }})</a></p>
    re_path('(?P<username>\w+)/(?P<article_id>\d+)', views.article_detail),  # 文章详情页
    re_path('(?P<username>\w+)', views.site),  # 博客站点
]

3、app02  urls

from django.urls import path, re_path, include
from app02 import views

urlpatterns = [
    path('home/', views.home),
    path('article_list/', views.article_list, name='article_list'),
    path('add_article/', views.add_article),
    path('del_article/', views.del_article),
    path('update_article/', views.update_article),
    path('upload_image/', views.upload_image),
    path('cate_article/', views.cate_article),
    path('tags_article/', views.tags_article),
]

4、html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js"></script>
    <style>
        .bingpai {
            line-height: 50px;
        }

        .bingpai {
            height: 100%;
        }

        .s1 {
            margin-right: 10px;
        }
    </style>
</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="/home/">前台首页</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  bingpai" >
                {% if request.session.username %}
                    <li><img style="border-radius: 50%" src="/media/{{ avatar }}" id="my_img" width="25px" alt=""></li>
                    <li class="active"><a href="/{{ request.session.username }}/">{{ request.session.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="#">修改头像</a></li>
                            <li><a href="/logout/">退出登录</a></li>
                            <li><a href="#">后台系统</a></li>
                        </ul>
                    </li>
                {% else %}
                    <li><a href="/register/">注册</a></li>
                    <li><a href="/login/">登录</a></li>
                {% endif %}
            </ul>
            <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">
                        <div class="row">
                            <h1 class="text-center">修改密码</h1>
                            <div class="col-md-8 col-md-offset-2">
                                <div class="form-group">
                                    用户名: <input type="text" class="form-control" readonly id="username"
                                                value="{{ request.session.username }}">
                                </div>
                                <div class="form-group">
                                    原密码: <input type="password" class="form-control" id="old_password">
                                </div>
                                <div class="form-group">
                                    新密码: <input type="password" class="form-control" id="new_password">
                                </div>
                                <div class="form-group">
                                    确认密码: <input type="password" class="form-control" id="re_password">
                                </div>
                                <div class="form-group">
                                    <input type="button" value="修改密码" class="btn btn-success btn-block btn_password">
                                </div>
                            </div>

                        </div>
                    </div>
                </div>
            </div>
        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>


<div class="container-fluid">
    <div class="row">
        <div class="col-md-3">
            <div class="list-group">
                <a href="/app02/home/" class="list-group-item active">
                    首页
                </a>
                <a href="/app02/article_list/" class="list-group-item">文章列表</a>
                <a href="/app02/cate_article/" class="list-group-item">分类列表</a>
                <a href="/app02/tags_article/" class="list-group-item">标签列表</a>
                <a href="#" class="list-group-item">更多</a>
            </div>
        </div>

        <div class="col-md-9">
                <div class="panel panel-info">
                    <div class="panel-heading">学习使人进步</div>
                    <div class="panel-body">
                        {% block content %}
                        <div class="jumbotron">
                            <h1>欢迎来到BBS后台页面</h1>
                            <p>你想要的这里都有</p>
                            <p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more</a></p>
                        </div>
                        <div class="row">
                            <div class="col-sm-6 col-md-4">
                                <div class="thumbnail">
                                    <img src="/static/img/qingxin.jpeg"
                                         alt="...">
                                    <div class="caption">
                                        <h3>Thumbnail label</h3>
                                        <p>...</p>
                                        <p><a href="#" class="btn btn-primary" role="button">Button</a> <a href="#"
                                                                                                           class="btn btn-default"
                                                                                                           role="button">Button</a>
                                        </p>
                                    </div>
                                </div>
                            </div>
                            <div class="col-sm-6 col-md-4">
                                <div class="thumbnail">
                                    <img src="/static/img/qingxin.jpeg"
                                         alt="...">
                                    <div class="caption">
                                        <h3>Thumbnail label</h3>
                                        <p>...</p>
                                        <p><a href="#" class="btn btn-primary" role="button">Button</a> <a href="#"
                                                                                                           class="btn btn-default"
                                                                                                           role="button">Button</a>
                                        </p>
                                    </div>
                                </div>
                            </div>
                            <div class="col-sm-6 col-md-4">
                                <div class="thumbnail">
                                    <img src="/static/img/qingxin.jpeg"
                                         alt="...">
                                    <div class="caption">
                                        <h3>Thumbnail label</h3>
                                        <p>...</p>
                                        <p><a href="#" class="btn btn-primary" role="button">Button</a> <a href="#"
                                                                                                           class="btn btn-default"
                                                                                                           role="button">Button</a>
                                        </p>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                    {% endblock %}
                </div>
        </div>
    </div>
</div>

{% block js %}

{% endblock %}
</body>
</html>

5、views

def home(request):
    # 获取用户头像地址
    avatar = models.UserInfo.objects.get(username=request.session.get('username')).avatar
    print(avatar)
    return render(request, 'backend/home.html', locals())

6、后台主页面

八、文章展示页

1、html

{% extends 'backend/home.html' %}

{% block content %}
    <h1 class="text-center">文章列表</h1>
    <a href="/app02/add_article/" class="btn btn-success" style="margin-bottom: 10px">添加文章</a>
    <table class="table-hover table table-striped table-bordered">
        <thead>
        <tr>
            <th>标题</th>
            <th>点赞数</th>
            <th>点踩数</th>
            <th>评论数</th>
            <th>操作</th>
        </tr>
        </thead>
        <tbody>
        {% for article in article_list %}
            <tr>
                <td><a href="/{{ request.session.username }}/{{ article.pk }}" target="_blank">{{ article.title }}</a></td>
                <td>{{ article.up_num }}</td>
                <td>{{ article.down_num }}</td>
                <td>{{ article.comment_num }}</td>
                <td>
                    <a href="/app02/update_article/?id={{ article.id }}" class="btn btn-success">修改</a>
                    <a href="/app02/del_article/?id={{ article.id }}" class="btn btn-danger">删除</a>
                </td>
            </tr>
        {% endfor %}
        </tbody>
    </table>
{% endblock %}

2、views

def article_list(request):
    article_list = models.Article.objects.all()
    return render(request, 'backend/article_list.html', locals())

3、添加文章html

{% extends 'backend/home.html' %}
{% block css %}
    {#<link rel="stylesheet" href="../themes/simple/simple.css" />#}
{% endblock %}

{% block content %}
    <h1 class="text-center">添加文章</h1>
    <form action="">
        <div class="form-group">
            文章标题: <input type="text" id="title" class="form-control">
        </div>
        <div class="form-group">
            文章内容: <textarea id="editor_id" name="content" style="width:100%;height:400px;"></textarea>
        </div>
        <div class="form-group">
            文章分类:
            <select name="" id="cate" class="form-control">
                {% for cate in cate_list %}
                    <option value="{{ cate.pk }}">{{ cate.name }}</option>
                {% endfor %}
            </select>
        </div>
        <div class="form-group">
            {% for tag in tags_list %}
                {{ tag.name }}<input type="checkbox" value="{{ tag.pk }}" name="tags">
            {% endfor %}
        </div>
        <div class="form-group">
            <input type="button" id="title" class="btn btn-success btn-block btn_article" value="提交">
        </div>
    </form>

{% endblock %}


{% block js %}
    {% load static %}
    <script charset="utf-8" src="{% static 'kindeditor/kindeditor-all-min.js' %}"></script>
    <script charset="utf-8" src="{% static 'kindeditor/lang/zh-CN.js' %}"></script>
    <script src="{% static 'layer-v3.5.1/layer/layer.js' %}"></script>
    <script>
        KindEditor.ready(function (K) {
            window.editor = K.create('#editor_id', {
                {#width: '800px',#}
                height: '500px',
                {#minWidth:'300px'#}
                items: [
                    'source', '|', 'undo', 'redo', '|', 'preview', 'print', 'template', 'code', 'cut', 'copy', 'paste',
                    'plainpaste', 'wordpaste', '|', 'justifyleft', 'justifyright',
                    'justifyfull', 'insertorderedlist', 'insertunorderedlist', 'indent', 'outdent', 'subscript',
                    'superscript', 'clearhtml', 'quickformat', 'selectall', '|', 'fullscreen', '/',
                    'formatblock', 'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold',
                    'italic', 'underline', 'strikethrough', 'lineheight', 'removeformat', '|', 'image', 'multiimage',
                    'flash', 'media', 'insertfile', 'table', 'hr', 'emoticons', 'baidumap', 'pagebreak',
                    'anchor', 'link', 'unlink', '|', 'about'
                ],
                resizeType: 0,
                {#themeType : 'simple'#}
                colorTable: [
                    ['#E53333', '#E56600', '#FF9900', '#64451D', '#DFC5A4', '#FFE500'],
                    ['#009900', '#006600', '#99BB00', '#B8D100', '#60D978', '#00D5FF'],
                    ['#337FE5', '#003399', '#4C33E5', '#9933E5', '#CC33E5', '#EE33EE'],
                ],
                uploadJson: '/app02/upload_image/',
                extraFileUploadParams: {
                    csrfmiddlewaretoken: '{{ csrf_token }}'
                }
            });
        });

        // 发起ajax请求
        $(".btn_article").click(function () {
            editor.sync();
            var title = $("#title").val();
            var content = $('#editor_id').val(); // jQuery
            var cate_id = $("#cate").val();
            var tags = $("input[name='tags']:checked"); // 可能就会有多个值的情况

            var tags_arr = [];
            $.each(tags, function (index, value) {
                tags_arr.push($(this).val());
            });

            console.log(tags_arr); // [1, 2, 3] =====> 1,2,3
            // 数组转为字符串
            var tags_s = tags_arr.join(','); // '1,2,3'
            console.log(tags_s);
            $.ajax({
                url: '',
                type: 'post',
                data: {
                    title: title,
                    content: content,
                    cate_id: cate_id,
                    tags: tags_s,
                    csrfmiddlewaretoken: '{{ csrf_token }}'
                },
                success: function (res) {
                    if (res.code === 200) {
                        layer.msg(res.msg, {}, function () {
                            location.href = '/app02/article_list/';
                        })
                    } else {
                        layer.msg(res.msg, {},)
                    }
                }
            })
        })

    </script>
{% endblock %}

4、添加文章views

def add_article(request):
    user_obj = models.UserInfo.objects.filter(pk=request.session.get('id')).first()
    if not user_obj:
        return redirect('/login/')
    blog = user_obj.blog

    # 查询所有文章的分类列表
    cate_list = models.Category.objects.all()

    # 查询所有的标签列表
    tags_list = models.Tag.objects.all()

    # 查询标签列表
    if request.method == 'POST':
        # 定义返回给前端的json数据格式
        back_dict = {'code': 200, 'msg': '添加成功,3秒后自动跳转页面'}

        # 接收前端传递过来的参数
        title = request.POST.get('title')
        cate_id = request.POST.get('cate_id')
        content = request.POST.get('content')
        tags = request.POST.get('tags')
        # print(tags, type(tags))  # 2,4 <class 'str'>
        # 需要将字符串转为列表使用
        tags_list = tags.split(',')  # [2,4]

        # 验证参数
        if not title:
            back_dict['code'] = 6001
            back_dict['msg'] = '标题不能为空'
            return JsonResponse(back_dict)
        if not cate_id:
            back_dict['code'] = 6002
            back_dict['msg'] = '分类必须要选'
            return JsonResponse(back_dict)
        if not cate_id:
            back_dict['code'] = 6003
            back_dict['msg'] = '标签必须要选'
            return JsonResponse(back_dict)

        '''
        1.摘要截取的问题
        2.xss攻击的问题:
            原理:有了script标签,把提交过来的内容过滤出script标签,然后做删除
            解决方法:使用正则匹配script,匹配到之后,做删除
            或者利用第三方模块来处理: bs4模块
            pip install beautifulsoup4
            BeautifulSoup它是在爬虫里面,他能够筛选数据,清洗数据,html数据
            BeautifulSoup(数据,解析器)
        '''
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(content, 'html.parser')
        # print(soup.find_all('a'))
        for tag in soup.find_all():
            # 获取所有的标签
            # print(tag.name)

            # 如果标签是script则使用decompose()方法删除
            if tag.name == 'script':
                tag.decompose()

        # 数据入库
        # 操作文章表、文章和标签的第三张表

        # 文章摘要
        # print(soup.text)  # 文本
        # print(soup.contents)  # 二进制的
        desc = soup.text[0: 100]

        # create的结果就是新增的 obj 对象
        article_obj = models.Article.objects.create(title=title, content=str(soup),
                                                    desc=desc, category_id=cate_id, blog=blog)
        # 操作第三张表,批量操作数据bulk_create
        articletag2obj_list = []
        for i in tags_list:
            article2tag_obj = models.Article2Tag(article_id=article_obj.pk, tag_id=i)
            articletag2obj_list.append(article2tag_obj)
        models.Article2Tag.objects.bulk_create(articletag2obj_list)

        return JsonResponse(back_dict)

    return render(request, 'backend/add_article.html', locals())

5、修改html

{% extends 'backend/home.html' %}

{% block content %}
    <h1 class="text-center">更新文章</h1>
    <div class="form-group form-control">
        文章标题:<input type="text" name="title" id="title" value="{{ article.title }}" style="width: 500px">
    </div>
    <div class="form-group form-control">
        点赞数:<input type="text" name="title" id="up" value="{{ article.up_num }}" style="width: 500px">
    </div>
    <div class="form-group form-control">
        点踩数:<input type="text" name="title" id="down" value="{{ article.down_num }}" style="width: 500px">
    </div>
    <div class="form-group form-control">
        评论数:<input type="text" name="title" id="comment" value="{{ article.comment_num }}" style="width: 500px">
    </div>
    <div>
        <button id="button_tj">提交</button>
    </div>
{% endblock %}

{% block js %}
    {% load static %}
    <script src="{% static 'layer-v3.5.1/layer/layer.js' %}"></script>
    <script>
        $("#button_tj").click(function () {
            {#alert(22)#}
            var title = $("#title").val()
            var up_num = $("#up").val()
            var down_num = $("#down").val()
            var comment_num = $("#comment").val()

            console.log(title)
            $.ajax({
                url: '',
                type: "POST",
                data: {
                    'title': title,
                    'up_num': up_num,
                    'down_num': down_num,
                    'comment': comment_num,
                    csrfmiddlewaretoken: '{{ csrf_token }}'
                },
                success: function (res) {
                    if (res.code === 200) {
                        layer.msg(res.msg, {}, function () {
                            location.href = '/app02/article_list/';
                        })
                     } else {
                        layer.msg(res.msg, {},)
                    }
                }
            })
        })
    </script>
{% endblock %}

6、修改views

def update_article(request):
    # 获取前端GET方式传来的id
    article_id = request.GET.get('id')
    # 用于前端展示原来的数据
    article = models.Article.objects.filter(pk=article_id).first()

    # 接收ajax传来数据
    if request.method == 'POST':
        title = request.POST.get('title')
        up_num = request.POST.get('up_num')
        down_num = request.POST.get('down_num')
        comment_num = request.POST.get('comment')

        # 更新数据库
        models.Article.objects.filter(pk=article_id).update(title=title, up_num=up_num, down_num=down_num, comment_num=comment_num)

        # 给前端返回提示语
        back_dict = {'code': 200, 'msg': '修改成功'}

        return JsonResponse(back_dict)

    return render(request, 'backend/update_article.html', locals())

7、删除views

def del_article(request):
    article_id = request.GET.get('id')
    # 删除文章(级连更新级连删除)
    models.Article.objects.filter(id=article_id).delete()

    # article2tag_obj = models.Article2Tag.objects.filter(article_id=article_id).first()
    # if article2tag_obj:
    #     id = article2tag_obj.pk
    #     # 删除tag和article第三张表记录
    #     models.Article2Tag.objects.filter(article_id=id).delete()

    return redirect('article_list')  # 这里用的是路由的反向解析
    # return redirect('app02/article')  # 有多个app应用,重定向先从外面app进来,不直接走templates

 

标签:项目,models,list,get,request,article,BBS,id
From: https://www.cnblogs.com/dgp-zjz/p/17628645.html

相关文章

  • 敏捷方法进行项目管理的几个关键步骤
    敏捷项目管理是一种基于敏捷开发方法的项目管理方式,它强调快速响应变化、持续交付价值和高效的团队合作。1、确定敏捷宣言的价值观和原则,例如“以人为本”、“可用的软件”、“以客户为中心”、“拥抱变化”等,并在项目中始终遵循这些价值观和原则。敏捷开发宣言:敏捷开发十二原则:2、......
  • idea 2021创建java web项目
    1创建普通Java项目2添加框架2.1添加框架2.2选择webapplication2.3新建如下文件夹在WEB-INF目录下,新建classes和lib文件夹,分别用于之后存字节码文件和jar包3编辑项目结构设置相关文件保存路径3.1设置编译文件保存路径3.2设置jar包保存路径12344t......
  • 项目管理思考-0828
    1收集需求是项目成功中的一个重要因素,需求越多,做的越多出错的可能性越多。2收集需求沟通技巧WHATWHYHOW。客户考虑是是做什么,技术考虑是怎么做,项目经理更应该考虑客户为什么这么做。举个例子,客户想要更快的一匹马,若是技术人员研究方法变成了一直研究马更快的事情,若是与客户......
  • 软件项目测试报告如何评估费用,软件测试详细方案分享
    软件项目测试报告评估 软件项目测试报告的费用评估通常是根据测试范围、测试复杂度、测试功能点、测试工作量、测试机构的经验和知名度等因素来评估的。一、常用的软件项目测试报告评估方法:1、测试范围评估:根据项目的需求和目标,评估测试范围的大小和复杂度,以确定测试工作量......
  • 如何将现有的`Blazor`项目的主题切换写的更好看?
    如何将现有的Blazor项目的主题切换写的更好看?在现有的系统当中,我们的主题切换会比较生硬,下面我们将基于MasaBlazor实现好看的扩散主题切换的样式效果。安装MASA.TemplatedotnetnewinstallMASA.Template创建MasaBlazor项目打开vs2022选择serverapp模板打开wwwroot......
  • 如何在github或gitlab中将upstream项目中的新分支添加到fork后的origin项目中?
    1、问题:在gitlab或github中,将某项目进行了fork,fork后在自己的项目空间中就有了自己的origin项目,之后如果在源项目(upstream项目)中添加了新的分支,但是在自己的origin项目中不会被同步添加上,这就需要自己去添加这个分支(假如分支名为:new)。2、解决:要将新的分支添加到自己的origin项......
  • 【openEuler创新项目探索】一个Java端的向量化BLAS库VectorBLAS
    VectorBLAS简介VectorBLAS是一个使用Java语言实现的向量化BLAS高性能库,目前已在openEuler社区开源。VectorBLAS通过循环展开、矩阵分块和内存布局优化等算法优化,对BLAS函数进行了深度优化,并利用VectorAPIJDK提供的多种向量化API实现。可以理解为:VectorBLAS=VectorAPI+BLAS......
  • 乌班图20.04版本下idea打开项目报错闪退(记录)ideaIU-2023.2.1.tar.gz
    [553960]WARN-#c.i.i.c.t.p.v.p.StateDatabaseParser-Nosuitabledriverfoundforjdbc:sqlite:/root/.config/Code/User/globalStorage/state.vscdbjava.sql.SQLException:Nosuitabledriverfoundforjdbc:sqlite:/root/.config/Code/User/globalStorage/stat......
  • 5个前端开源项目带你在Web上体验Windows
    本文来分享五个yyds的开源项目,这些项目把Windows带到了Web平台上。让我们一起感受这些项目带来的回忆和创新,重温Windows93、98、XP和7的经典界面,甚至探索现代概念中的Windows11和12!Windows12使用JavaScript、CSS、HTML等技术开发的网页版Windows12,且支持深......
  • vue微信H5项目使用腾讯地图获取当前位置经纬度
    1.在index.html引入js文件<scriptsrc="https://3gimg.qq.com/lightmap/components/geolocation/geolocation.min.js"></script>2.在需要页面中你自己的key要去腾讯地图工具去申请https://lbs.qq.com/webApi/component/componentGuide/componentPickercreated(){this.getM......