模块六 博客系统实现


model 设计

from django.contrib.auth.models import AbstractUser
from django.db import models


# Create your models here.


class Userinfo(AbstractUser):
    """
    用户信息。关联auth认证
    """
    nid = models.AutoField(primary_key=True)
    telephone = models.CharField(max_length=11, null=True, unique=True)
    avatar = models.FileField(upload_to='avatars/', default="avatars/default.png")
    create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
    blog = models.OneToOneField(to="Blog", to_field="nid", null=True, on_delete=models.CASCADE)

    def __str__(self):
        return self.username


class Blog(models.Model):
    """
    博客信息
    """
    nid = models.AutoField(primary_key=True)
    title = models.CharField(max_length=32, verbose_name='个人博客标题')
    site_name = models.CharField(max_length=32, verbose_name='站点名称')
    theme = models.CharField(max_length=32, verbose_name='博客主题')

    def __str__(self):
        return self.title


class Category(models.Model):
    """个人博客分类表"""
    nid = models.AutoField(primary_key=True)
    title = models.CharField(verbose_name='分类标题', max_length=32)
    blog = models.ForeignKey(verbose_name="所属博客", to="Blog", to_field="nid", on_delete=models.CASCADE)

    def __str__(self):
        return self.title


class Tag(models.Model):
    """个人博客标签表"""
    nid = models.AutoField(primary_key=True)
    title = models.CharField(verbose_name='标签名称', max_length=32)
    blog = models.ForeignKey(verbose_name='所属博客', to='Blog', to_field="nid", on_delete=models.CASCADE)

    def __str__(self):
        return self.title


class Article(models.Model):
    nid = models.AutoField(primary_key=True)
    title = models.CharField(max_length=64, verbose_name="文章标题")
    desc = models.CharField(max_length=255, verbose_name="文章描述")
    create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
    content = models.TextField()

    comment_count = models.IntegerField(default=0, verbose_name="评论数")
    up_count = models.IntegerField(default=0)
    down_count = models.IntegerField(default=0)

    user = models.ForeignKey(verbose_name="作者", to="Userinfo", to_field="nid", on_delete=models.CASCADE)
    category = models.ForeignKey(to="Category", to_field="nid", null=True, on_delete=models.CASCADE)
    tags = models.ManyToManyField(to="Tag", through="Article2Tag", through_fields=("article", "tag"), )

    def __str__(self):
        return self.title


class Article2Tag(models.Model):
    """文章标签表"""
    nid = models.AutoField(primary_key=True)
    article = models.ForeignKey(verbose_name="文章", to="Article", to_field="nid", on_delete=models.CASCADE)
    tag = models.ForeignKey(verbose_name="标签", to="Tag", to_field="nid", on_delete=models.CASCADE)

    class Meta:
        unique_together = [
            ("article", "tag"),
        ]

    def __str__(self):
        v = self.article.title + "----" + self.tag.title
        return v


class ArticleUpDown(models.Model):
    """点赞表"""
    nid = models.AutoField(primary_key=True)
    user = models.ForeignKey('Userinfo', null=True, on_delete=models.CASCADE)
    article = models.ForeignKey("Article", null=True, on_delete=models.CASCADE)
    is_up = models.BooleanField(default=True)

    class Meta:
        unique_together = [
            ('article', 'user'),
        ]


class Comment(models.Model):
    """评论表"""
    nid = models.AutoField(primary_key=True)
    article = models.ForeignKey(verbose_name="评论文章", to="Article", to_field="nid", on_delete=models.CASCADE)
    user = models.ForeignKey(verbose_name="评论者", to="Userinfo", to_field="nid", on_delete=models.CASCADE)
    content = models.CharField(verbose_name="评论内容", max_length=255)
    create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
    parent_comment = models.ForeignKey('self', null=True, on_delete=models.CASCADE)

    def __str__(self):
        return self.content

核心逻辑

import json
import os
import threading

from bs4 import BeautifulSoup
from django.contrib import auth
from django.contrib.auth.decorators import login_required
from django.core.mail import send_mail
from django.db import transaction
from django.db.models import F
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, redirect

# Create your views here.
from blog import models
from blog.models import Userinfo
from blog.myforms import UserForms
from blog.utils import validCode
from blogsys import settings


def login(request):
    if request.method == "POST":
        res_data = {"user": None, "msg": None}
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        vaild_code = request.POST.get("valid_code")

        valid_code_str = request.session.get("valid_code_str")
        if vaild_code.upper() == valid_code_str.upper():
            user = auth.authenticate(username=user, password=pwd)
            if user:
                auth.login(request, user)  # request.userobj == request.user
                res_data["user"] = user.username
            else:
                res_data["msg"] = "用户名or密码错误!"
        else:
            res_data["msg"] = "验证码错误"

        return JsonResponse(res_data)

    return render(request, "login.html")


@login_required
def index(request):
    """
    系统首页
    :param request:
    :return:
    """
    article_list = models.Article.objects.all()
    return render(request, "index.html", {'article_list': article_list})


def logout(request):
    """
    注销
    :param request:
    :return:
    """
    auth.logout(request)
    return redirect("/login/")


def register(request):
    if request.method == "POST":
        if request.is_ajax():
            print(request.POST)
            form = UserForms(request.POST)
            response_data = {"user": None, "msg": None}

            if form.is_valid():
                response_data["user"] = form.cleaned_data.get("user")
                # 创建一条用户记录
                user = form.cleaned_data.get('user')
                pwd = form.cleaned_data.get('pwd')
                email = form.cleaned_data.get('email')
                telephone = form.cleaned_data.get("telephone")
                avatar_obj = request.FILES.get("avatar")
                # print("avatar_obj", avatar_obj) == 文件名
                file_path_name = os.path.join(settings.MEDIA_ROOT, "avatars", avatar_obj.name)
                with open(file_path_name, 'wb') as f:
                    for line in avatar_obj:
                        f.write(line)
                extra = {}
                if avatar_obj:
                    extra["avatar"] = avatar_obj
                Userinfo.objects.create_user(username=user, password=pwd, email=email, telephone=telephone, **extra)
            else:
                print(form.cleaned_data)
                print(form.errors)
                response_data["msg"] = form.errors

            return JsonResponse(response_data)

    form = UserForms()
    return render(request, "register.html", {"form": form})


def get_validcode_img(request):
    """
    基于PIL模块动态生效相应状态码图片
    :param request:
    :return:
    """
    img_data = validCode.get_valid_code_img(request)
    # with open('ceshi.png','wb') as f:
    #     for line in img_data:
    #         f.write(line)

    return HttpResponse(img_data)


@login_required
def home_site(request, username, **kwargs):
    # 区分站点页面 or 站点下的跳转页面
    print("kwargs", kwargs)
    print("username", username)
    user = Userinfo.objects.filter(username=username).first()
    if not user:
        return render(request, "not_found.html")
    blog = user.blog
    # 当前用户 or 站点对应的文章
    article_list = models.Article.objects.filter(user=user)
    if kwargs:
        condition = kwargs.get("condition")
        param = kwargs.get("param")  # 2021-05
        if condition == "category":
            # 登录用户下 指定分类对应的文章
            article_list = article_list.filter(category__title=param)
            # 登录用户下 指定标签对应的文章
        elif condition == "tag":
            article_list = article_list.filter(tags__title=param)
            '''
            SELECT
                `blog_article`.`nid`,
                `blog_article`.`title`,
                `blog_article`.`desc`,
                `blog_article`.`create_time`,
                `blog_article`.`content`,
                `blog_article`.`comment_count`,
                `blog_article`.`up_count`,
                `blog_article`.`down_count`,
                `blog_article`.`user_id`,
                `blog_article`.`category_id` 
            FROM
                `blog_article`
                INNER JOIN `blog_article2tag` ON ( `blog_article`.`nid` = `blog_article2tag`.`article_id` )
                INNER JOIN `blog_tag` ON ( `blog_article2tag`.`tag_id` = `blog_tag`.`nid` ) 
            WHERE
                (
                    `blog_article`.`user_id` = 1 
                    AND `blog_tag`.`title` = 'python' 
                );
            args = ( 1, 'python' )
            '''
        else:
            year, month = param.split("/")
            article_list = article_list.filter(create_time__year=year, create_time__month=month)

    return render(request, "home_site.html", {"username": username, "blog": blog, "article_list": article_list})


@login_required
def article_detail(request, username, article_id):
    """
    文章详情页
    :param request:
    :param username:
    :param article_id:
    :return:
    """
    user = Userinfo.objects.filter(username=username).first()
    blog = user.blog
    article_obj = models.Article.objects.filter(pk=article_id).first()
    comment_list = models.Comment.objects.filter(article_id=article_id)

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


def get_comment_tree(request):
    """评论树"""
    article_id = request.GET.get("article_id")
    res_data = list(models.Comment.objects.filter(article_id=article_id).order_by("pk").values("pk", "content",
                                                                                               "parent_comment_id"))
    print("res_data", res_data)
    # res_data 列表 需要加上safe=False  只能传递字典
    return JsonResponse(res_data, safe=False)


@login_required
def cn_backend(request):
    """后台管理"""
    article_list = models.Article.objects.filter(user=request.user)
    return render(request, "backend/backend.html", locals())


@login_required
def add_article(request):
    """
    后台管理中添加内容
    :param request:
    :return:
    """
    if request.method == "POST":
        title = request.POST.get("title")
        content = request.POST.get("content")

        # 防止xss攻击
        soup = BeautifulSoup(content, "html.parser")
        for tag in soup.find_all():
            print(tag.name)
            if tag.name == "script":
                tag.decompose()
        # 构建摘要数据,取标签字符串文本前150个字符
        desc = soup.text[0:150] + "..."
        models.Article.objects.create(title=title, desc=desc, content=str(soup), user=request.user)
        print("content==>", str(soup))
        return redirect("/cn_backend/")
    return render(request, "backend/add_article.html")


@login_required
def delete_article(request, delete_article_id):
    """后台删除文章"""
    models.Article.objects.filter(pk=delete_article_id).delete()
    return redirect("/cn_backend/")


@login_required
def edit_article(request, edit_article_id):
    """后台编辑文件"""
    edit_article_obj = models.Article.objects.filter(pk=edit_article_id).first()

    if request.method == "POST":
        title = request.POST.get("title")
        content = request.POST.get("content")
        models.Article.objects.filter(pk=edit_article_id).update(title=title, content=content)
        return redirect("/cn_backend/")

    return render(request, "backend/edit_article.html", {"edit_article_obj": edit_article_obj})


def upload(request):
    """
    编辑器文件上传
    :param request:
    :return:
    """
    print(request.FILES)
    img_obj = request.FILES.get("upload_img")
    print(img_obj.name)
    path = os.path.join(settings.MEDIA_ROOT, "add_article_img", img_obj.name)
    with open(path, "wb") as f:
        for line in img_obj:
            f.write(line)
    # return HttpResponse('ok')

    response = {
            'error': 0,
            'url': '/media/add_article_img/%s' % img_obj.name
        }
    return HttpResponse(json.dumps(response))


def digg(request):
    """点赞"""
    print(request.POST)
    article_id = request.POST.get("article_id")
    is_up = json.loads(request.POST.get("is_up"))
    user_id = request.user.pk
    obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()

    res_data = {"status": True}
    if not obj:
        models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
        queryset_id = models.Article.objects.filter(pk=article_id)
        if is_up:
            queryset_id.update(up_count=F("up_count") + 1)
        else:
            queryset_id.update(down_count=F("down_count") + 1)
    else:
        res_data['status'] = False
        res_data['handled'] = obj.is_up

    return JsonResponse(res_data)


def comment(request):
    """评论"""
    print(request.POST)

    article_id = request.POST.get("article_id")
    pid = request.POST.get("pid")
    content = request.POST.get("content")
    user_id = request.user.pk

    article_obj = models.Article.objects.filter(pk=article_id).first()

    # 事务操作
    with transaction.atomic():
        comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,
                                                    parent_comment_id=pid)
        models.Article.objects.filter(pk=article_id).update(comment_count=F("comment_count") + 1)

    res_data = {}

    res_data["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")
    res_data["username"] = comment_obj.user.username
    res_data["content"] = content

    # 发送邮件
    # send_mail(
    #     "您的文章%s新增了一条评论内容" % article_obj.title,
    #     content,
    #     settings.EMAIL_HOST_USER,
    #     ["li_xiang111@qq.com"]
    # )

    t = threading.Thread(target=send_mail, args=("您的文章%s新增了一条评论内容" % article_obj.title,
                                                 content,
                                                 settings.EMAIL_HOST_USER,
                                                 ["li_xiang111@qq.com"]))
    t.start()

    return JsonResponse(res_data)

myform 跟CRUD类似

模板新知识点

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author:lixiang
from django import template
from django.db.models import Count

from blog import models

register = template.Library()


@register.simple_tag
def multi_tag(x, y):
    return x * y


@register.inclusion_tag("classification.html")  # == 闭包传参 inclusion_tag(get_classification_style) 固定用法  渲染模板
def get_classification_style(username):
    user = models.Userinfo.objects.filter(username=username).first()
    blog = user.blog
    # 查询每个博客下面的分类标题的名称以及对应的文章数
    cate_list = models.Category.objects.filter(blog=blog).values("pk").annotate(c=Count("article__title")).values_list(
        "title", "c")
    # 查询对应用户博客的所有标签的标题名称和对应的文章数  不加__title就对article_id进行count,默认关联第三张表的tag.nid == article2tag.tag_id
    tag_list = models.Tag.objects.filter(blog=blog).values("pk").annotate(c=Count("article")).values_list("title", "c")

    date_list = models.Article.objects.filter(user=user).extra(
        select={"y_m_date": "date_format(create_time,'%%Y/%%m')"}).values("y_m_date").annotate(
        c=Count("nid")).values_list("y_m_date", "c")

    return {"blog": blog, "cate_list": cate_list, "date_list": date_list, "tag_list": tag_list, "username": username}


'''
SELECT
    `blog_category`.`title`,
    COUNT( `blog_article`.`title` ) AS `c` 
FROM
    `blog_category`
    LEFT OUTER JOIN `blog_article` ON ( `blog_category`.`nid` = `blog_article`.`category_id` ) 
WHERE
    `blog_category`.`blog_id` = 1 
GROUP BY
    `blog_category`.`nid` 
ORDER BY
NULL 
    LIMIT 21;

SELECT
    ( date_format( create_time, '%Y/%m' ) ) AS `y_m_date`,
    COUNT( `blog_article`.`nid` ) AS `c` 
FROM
    `blog_article` 
WHERE
    `blog_article`.`user_id` = 1 
GROUP BY
    ( date_format( create_time, '%Y/%m' ) ) 
ORDER BY
    NULL;


SELECT
    `blog_tag`.`title`,
    COUNT( `blog_article2tag`.`article_id` ) AS `c` 
FROM
    `blog_tag`
    LEFT OUTER JOIN `blog_article2tag` ON ( `blog_tag`.`nid` = `blog_article2tag`.`tag_id` ) 
WHERE
    `blog_tag`.`blog_id` = 1 
GROUP BY
    `blog_tag`.`nid` 
ORDER BY
    NULL;

'''

在对应模板中

<div>
    <div class="panel panel-warning">
        <div class="panel-heading">我的标签</div>
        <div class="panel-body">
            {% for tag in tag_list %}
                <p><a href="/{{ username }}/tag/{{ tag.0 }}">{{ tag.0 }}({{ tag.1 }})</a></p>
            {% endfor %}

        </div>
    </div>

    <div class="panel panel-danger">
        <div class="panel-heading">随笔分类</div>
        <div class="panel-body">
            {% for cate in cate_list %}
                <p><a href="/{{ username }}/category/{{ cate.0 }}">{{ cate.0 }}({{ cate.1 }})</a></p>
            {% endfor %}
        </div>
    </div>

    <div class="panel panel-success">
        <div class="panel-heading">随笔归档</div>
        <div class="panel-body">
            {% for date in date_list %}
                <p><a href="/{{ username }}/archive/{{ date.0 }}">{{ date.0 }}({{ date.1 }})</a></p>
            {% endfor %}
        </div>
    </div>
</div>

模板 - 编辑框

{% extends "backend/base.html" %}

{% block content %}

    <form action="" method="post">
        {% csrf_token %}
        <div class="add_article">
            <div class="alert-success text-center">添加文章</div>
            <div class="add_article_region">
                <div class="title form-group">
                    <label for="">标题</label>
                    <div>
                        <input type="text" name="title">
                    </div>
                </div>
                <div class="content form-group">
                    <label for="">内容(Kindeditor编辑器,不支持拖放/粘贴上传图片)</label>
                    <div>
                        <textarea name="content" id="article_content" cols="30" rows="10"></textarea>
                    </div>
                </div>
                <input type="submit" class="btn btn-default">
            </div>

        </div>


    </form>

    <script src="/static/js/jquery-3.2.1.min.js"></script>
    <script charset="utf-8" src="/static/blog/kindeditor/kindeditor-all.js"></script>

    <script>

        KindEditor.ready(function (K) {
            window.editor = K.create('#article_content', {
                width: "800",
                height: "600",
                resizeType: 0,
                uploadJson: "/upload/",
                extraFileUploadParams: {
                    csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val()
                },
                filePostName: "upload_img"

            });
        });

    </script>

{% endblock %}

登录

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="/static/blog/bs/css/bootstrap.css">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <form>
                {% csrf_token %}
                <div class="form-group">
                    <label for="user">用户名</label>
                    <input type="text" id="user" class="form-control">
                </div>
                <div class="form-group">
                    <label for="pwd">密码</label>
                    <input type="password" id="pwd" class="form-control">
                </div>


                <div class="form-group">
                    <label for="pwd">验证码</label>
                    <div class="row">
                        <div class="col-md-6">
                            <input type="text" class="form-control" id="valid_code">
                        </div>
                        <div class="col-md-6">
                            <img width="270" height="36" id="valid_code_img" src="/get_validCode_img/" alt="">
                        </div>
                    </div>


                </div>

                <input type="button" class="btn btn-default login_btn" value="submit"><span class="error"></span>
                <a href="/register/" class="btn btn-primary pull-right">注册</a>

            </form>


        </div>
    </div>
</div>


<script src="/static/js/jquery-3.2.1.min.js"></script>

<script>
    // 验证码刷新
    $("#valid_code_img").click(function () {
        //console.log($(this));
        $(this)[0].src += "?";

    })

    //登录验证

    $(".login_btn").click(function () {


        $.ajax({
            url: "",
            type: "post",
            data: {
                user: $("#user").val(),
                pwd: $("#pwd").val(),
                valid_code: $("#valid_code").val(),
                csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val(),
            },
            success: function (data) {

                console.log(data)
                if (data.user) {
                    // 获取url ? 后面的字符串
                    if (location.search) {
                        // slice(start,end)已有的数组中返回选定的元素
                        location.href = location.search.slice(6)
                    } else {
                        location.href = "/index/"
                    }
                } else {
                    $(".error").text(data.msg).css({"color": "red", "margin-left": "10px"});
                    setTimeout(function () {
                        $(".error").text("");

                    }, 2000)


                }

            }
        })

    })

</script>

</body>
</html>

注册

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="/static/blog/bs/css/bootstrap.css">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <form id="form">
                {% csrf_token %}
                {% for filed in form %}
                    <div class="form-group">
                        <label for="">{{ filed.label }}</label>
                        {{ filed }} <span class="error pull-right"></span>
                    </div>

                {% endfor %}


                <div class="form-group">
                    <label for="avatar">
                        头像
                        <img src="/static/blog/img/default.png" id="avatar_img" alt="">
                    </label>
                    <input type="file" id="avatar" name="avatar">
                </div>


                <input type="button" class="btn btn-default reg_btn" value="submit"><span class="error"></span>

            </form>

        </div>
    </div>
</div>


<script src="/static/js/jquery-3.2.1.min.js"></script>

<script>

    // 头像预览
    $("#avatar").change(function () {
        // 获取用户选中的文件对象
        var file_obj = $(this)[0].files[0];
        // 获取文件对象路径
        var reader = new FileReader();
        reader.readAsDataURL(file_obj);
        // 修改img src属性,src=文件对象的路径
        reader.onload = function () {
            $("#avatar_img").attr("src", reader.result);
        };

    })

    // ajax提交
    $(".reg_btn").click(function () {
        //获取form数据 [{},{},{}]
        var request_data = $("#form").serializeArray();
        // 建立提交formdata对象
        var formdata = new FormData();
        $.each(request_data, function (index, data) {
            formdata.append(data.name, data.value)
        });
        //加入文件对象
        formdata.append("avatar", $("#avatar")[0].files[0]);

        $.ajax({
            url: "",
            type: "post",
            contentType: false,
            processData: false,
            data: formdata,
            success: function (data) {

                console.log(data);
                if (data.user) {
                    //注册成功
                    window.location.href = "/login/"

                } else {
                    // 注册失败
                    console.log(data.msg)
                    // 清空错误信息
                    $("span.error").html("")
                    $(".form-group").removeClass("has_error");

                    //显示错误信息
                    $.each(data.msg, function (field, error_list) {
                        //console.log(field, error_list);
                        if (field == "__all__") {
                            $("#id_r_pwd").next().html(error_list[0]).parent().addClass("has-error");
                        } else {
                            $("#id_" + field).next().html(error_list[0]);
                            $("#id_" + field).parent().addClass("has-error");

                        }


                    })


                }

            }

        })

    })

</script>

</body>
</html>