IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Python知识库 -> 阶段总结之BBS -> 正文阅读

[Python知识库]阶段总结之BBS

目录

项目流程

1. 1 功能需求分析

1.2 技术栈

1.3 表关系梳理

表的关联关系

创建项目

表关系建立

迁移表

注册forms编写

路由配置

基于forms组件写的注册页面

头像设置

??使用Admin 去录入数据

个人站点页面的设计

?文章详细页的设计

?点赞,评论,评论树及其发送邮件

后台管理页面设计

小知识点补充

字段类的属性(字段类型)

ManyToManyField

没有安装mysqlclient会报错


学习完MySQL数据库和Django框架之后, 我们结合前后端来写一个项目.此项目主要是模仿博客园.

项目流程

1. 1 功能需求分析

? ? ? ? 1.注册功能

? ? ? ? 2.登录功能

? ? ? ? 3.个人主页: 文章展示,侧边栏过滤>> 按照时间,标签, 分类

? ? ? ? 4. 文章详情 : 点赞点踩. 评论

? ? ? ? 5.后台管理; 个人文案行展示[增删改查]

? ? ? ? 6.发布文章>> 富文本编辑器, xss攻击处理

1.2 技术栈

? ? ? ? python 3.8? ????????? django 2.2.2 ????????mysql 5.6.4 ????????jquery 2.x ????????bootstrap3

1.3 表关系梳理

  • 用户表
  • 博客表
  • 标签表
  • 分类表
  • 文章表
  • 点赞点踩表
  • 评论表

表的关联关系

  • 用户表[基于auth的user表扩写字段]
  • 博客表[ 与用户表一对一]
  • 标签表[跟博客表一对多, 跟文章表多对多]
  • 分类表[跟博客表一对多,跟文章表一对多]
  • 文章表[跟博客表一对多]
  • 点赞点踩表[跟用户一对多, 跟文章表一对多]
  • 评论表[跟用户一对多, 跟文章表一对多]

创建项目

创建django项目, 配置设置信息, 连接数据库等

# 时间国际化

LANGUAGE_CODE = 'Asia-Shanghai'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = False


# 连接数据库


DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'bbs',
        'HOST': '127.0.0.1',
        'PORT': 3306,
        'USER': 'root',
        'PASSWORD':'123',
        'CHARSET': 'UTF8'
        

    }
}



# 静态资源配置

STATIC_URL = '/static/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR,'static')
]

基于auth的user表扩写字段


class UserInfo(AbstractUser):
    phone = models.CharField(max_length=32,null=True,)
    avatar = models.FileField(upload_to='avatar',default='avatar/default.jpg')
    blog = models.OneToOneField(to='Blog',on_delete=models.CASCADE,null=True)

表关系建立

from django.db import models

# Create your models here.
from django.contrib.auth.models import AbstractUser


class UserInfo(AbstractUser):  # 继承AbstractUser表 只用写auth表中没有的字段
    phone = models.CharField(max_length=32, null=True, verbose_name='用户手机号')
    # upload_to是文件保存在什么路径
    icon = models.FileField(upload_to='icon/', default='icon/default.png', null=True, verbose_name='用户头像')
    # 用户表和博客表一对一
    blog = models.OneToOneField(to='Blog', on_delete=models.CASCADE, null=True)


class Blog(models.Model):
    title = models.CharField(max_length=32, null=True, verbose_name='主标题')
    site_title = models.CharField(max_length=32, null=True, verbose_name='副标题')
    site_style = models.CharField(max_length=64, null=True, verbose_name='站点样式')


class Tag(models.Model):
    name = models.CharField(max_length=32, verbose_name='标签名', null=True)
    # 标签和博客是一对多 一个博客有多个标签
    blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)


class Classify(models.Model):
    name = models.CharField(max_length=32, verbose_name='分类名')
    # 分类和博客是一对多关系 一个博客有多个分类
    blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)


class Article(models.Model):
    title = models.CharField(max_length=32, verbose_name='文章标题')
    desc = models.CharField(max_length=255, verbose_name='文章摘要')
    content = models.TextField(verbose_name='文章内容')
    create_time = models.DateTimeField(auto_now_add=True)  # 第一次创建时自动添加时间
    # 文章和分类表是一对多 一个分类有多篇文章
    classify = models.ForeignKey(to='Classify', on_delete=models.CASCADE)
    # 文章和标签是多对多关系 自动创建第三张表
    tag = models.ManyToManyField(to='Tag')
    # 文章和博客是一对多关系 一个博客对应多篇文章
    blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)


class UpAndDown(models.Model):
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='点赞点踩时间')
    # 和用户表是一对多关系 一个用户可以有多条点赞点踩记录
    user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
    # 和文章也是一对多
    article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
    # 1代表点赞 0代表点踩
    is_up = models.BooleanField(verbose_name='是否点赞')


class Comment(models.Model):
    content = models.CharField(max_length=64, verbose_name='评论内容')
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
    user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
    article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
    # 自关联字段 只能存已有评论的主键值
    parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
    # 自关联的其他方式
    # parent = models.ForeignKey(to='Comment', on_delete=models.CASCADE)
    # parent = models.IntegerField(null=Ture)

迁移表

python manage.py  makemigrations

python manage.py migrate

注册forms编写

from django import forms
from django.forms import widgets
from blog.models import UserInfo
from django.core.exceptions import ValidationError  # 合法性错误


class User(forms.Form):
    # 用户名 密码 确认密码 邮箱
    username = forms.CharField(max_length=8, min_length=3, label='用户名', required=True,
                               error_messages={'max_length': '用户名最多只能输入8位',
                                               'min_length': '用户名最少输入3位',
                                               'required': '用户名必须填'
                                               },
                               # 添加bootstr样式
                               widget=widgets.TextInput(attrs={'class': 'form-control'})
                               )
    password = forms.CharField(max_length=16, min_length=8, required=True, label='密码',
                               error_messages={
                                   'max_length': '密码最长16位',
                                   'min_length': '密码最短8位',
                                   'required': '密码不能为空',
                               },
                               widget=widgets.PasswordInput(attrs={'class': 'form-control'})
                               )
    re_password = forms.CharField(max_length=16, min_length=8, required=True, label='密码',
                                  error_messages={
                                      'max_length': '密码最长16位',
                                      'min_length': '密码最短8位',
                                      'required': '密码不能为空',
                                  },
                                  widget=widgets.PasswordInput(attrs={'class': 'form-control'})
                                  )
    email = forms.EmailField(label='邮箱地址', widget=widgets.EmailInput(attrs={'class': 'form-control'}))

    # 局部钩子 校验用户名是否存在
    def clean_username(self):
        name = self.cleaned_data.get('username')
        if UserInfo.objects.filter(username=name).first():
            # 用户已存在
            raise ValidationError('用户名已存在')  # 校验错误抛出异常
        else:
            return name

    # 局部钩子 校验用户名是否存在
    # def clean_username(self):
    #     username = self.cleaned_data.get('username')
    #     try:
    #         UserInfo.objects.get(username=username)
    #         print(UserInfo.objects.get(username=username), type(UserInfo.objects.get(username=username)))
    #         raise ValidationError('用户名已存在')
    #     except Exception:
    #         return username

    # 全局钩子 校验两次输入密码是否一致
    def clean(self):
        pwd = self.cleaned_data.get('password')
        re_pwd = self.cleaned_data.get('re_password')
        if pwd != re_pwd:
            raise ValidationError('两次密码不一致')  # 主动抛出合法性错误
        else:
            return self.cleaned_data

路由配置

from django.contrib import admin
from django.urls import path, re_path
from blog import views
from cnblog_review import settings
from django.views.static import serve
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.login),
    path('get_validCode_image/', views.get_validCode_image),
    re_path(r'^$', views.index),
    path('register/', views.register),
    path('logout/', views.logout),
     
    # 点赞
    path('digg/', views.digg),
    # 评论
    path('comment/', views.comment),
    # 树形评论
    path('get_comment_tree/', views.get_comment_tree),
     
    # media配置
    re_path(r'media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
     
    re_path(r'^(?P<username>\w+)/articles/(?P<article_id>\d+)$', views.article_detail),
     
    # 后台管理url
    re_path(r'cn_backend/$', views.cn_backend),
    re_path(r'cn_backend/add_article/$', views.add_article),
     
    # 关于个人站点的URL
    re_path(r'^(?P<username>\w+)/$', views.home_site),
     
    # 关于个人站点的跳转
    re_path(r'^(?P<username>\w+)/(?P<condition>tag|category|archive)/(?P<param>.*/$)', views.home_site),
 

基于forms组件写的注册页面

views.py

def get_validCode_image(request):
    """
    基于PIL模块动态生成响应状态码图片
    :param request:
    :return:
    """
    data = get_valid_code_img(request)
    return HttpResponse(data)
 
 
def index(request):
    """
    系统首页
    :param request:
    :return:
    """
    article_list = models.Article.objects.all()
 
    return render(request, 'index.html', locals())
 
 
def logout(request):
    """
    注销视图
    :param request:
    :return:
    """
    auth.logout(request)
    # 等同于执行了  request.session.fulsh()
    return redirect('/login/')
 
 
def register(request):
    """
    注册视图函数:
       get请求响应注册页面
       post(Ajax)请求,校验字段,响应字典
    :param request:
    :return:
    """
    if request.is_ajax():
        print(request.POST)
        form = UserForm(request.POST)
 
        response = {'user': None, 'msg': None}
        if form.is_valid():
            response['user'] = form.cleaned_data.get('user')
 
            #  生成一条用户记录
            user = form.cleaned_data.get('user')
            pwd = form.cleaned_data.get('pwd')
            email = form.cleaned_data.get('email')
            avatar_obj = request.FILES.get('avatar')
             
            extra = {}
            if avatar_obj:
                extra['avatar'] = avatar_obj
            # 要是逻辑没有用到值,我们可以不用赋值,等用到的时候,则添加
            UserInfo.objects.create_user(username=user, password=pwd, email=email, avatar=avatar_obj, **extra)
 
 
        else:
            print(form.cleaned_data)
            print(form.errors)
            response['msg'] = form.errors
 
        return JsonResponse(response)
 
    form = UserForm()
    return render(request, 'register.html', locals())

 register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="/static/CSS/register.css">
    <link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.css">
    <script src="/static/JS/jquery-3.2.1.min.js"></script>
 
 
</head>
<body>
<h3 class="text-center">注册页面</h3>
<div class="container">
    <div class="row">
        <div class="col-md-6 col-lg-offset-3">
 
            <form id="form">
                {% csrf_token %}
 
                {% for field in form %}
                    <div class="form-group">
                        <label for="{{ field.auto_id }}">{{ field.label }}</label>
                        {{ field }} <span class="error pull-right"></span>
                    </div>
                {% endfor %}
 
                <div class="form-group">
                    <label for="avatar">
                        头像
                        <img id="avatar_img" width="60" height="60" src="/static/img/default.png" 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>
    // 头像预览
    $("#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 () {
        //console.log($("#form").serializeArray());
        var formdata = new FormData();
        var request_data = $("#form").serializeArray();
        $.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) {
                    // 注册成功
                    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_re_pwd").next().html(error_list[0]).parent().addClass("has-error");
                        }
                        $("#id_" + field).next().html(error_list[0]);
                        $("#id_" + field).parent().addClass("has-error");
 
 
                    })
 
                }
            }
        })
 
    })
 
 
</script>
 
</body>
</html>

 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="/static/CSS/index.css">
    <link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.css">
    <script rel="stylesheet" src="/static/JS/jquery-3.2.1.js"></script>
    <script rel="stylesheet" src="/static/bootstrap-3.3.7/dist/js/bootstrap.min.js"></script>
 
</head>
<body>
 
<nav class="navbar navbar-default">
  <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="#">博客园</a>
    </div>
 
    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li class="active"><a href="#">随笔<span class="sr-only">(current)</span></a></li>
        <li><a href="#">新闻</a></li>
        <li><a href="#">博文</a></li>
 
      </ul>
 
      <ul class="nav navbar-nav navbar-right">
          {% if request.user.is_authenticated %}
              <li><a href="#"><span id="user_icon" class="glyphicon glyphicon-user"></span>{{ request.user.username }}</a></li>
              <li class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
              <ul class="dropdown-menu">
                    <li><a href="#">修改密码</a></li>
                    <li><a href="#">修改头像</a></li>
                    <li><a href="/logout/">注销</a></li>
                    <li role="separator" class="divider"></li>
                    <li><a href="#">Separated link</a></li>
              </ul>
            </li>
          {% else %}
              <li><a href="/login/">登录</a> </li>
              <li><a href="/register/">注册</a> </li>
          {% endif %}
      </ul>
    </div>
  </div>
</nav>
 
<div class="container-fluid">
    <div class="row">
        <div class="col-md-3">
            <div class="panel panel-warning">
                <div class="panel-heading">Panel heading without title</div>
                <div class="panel-body">
                    Panel content
                </div>
            </div>
            <div class="panel panel-info">
                <div class="panel-heading">Panel heading without title</div>
                <div class="panel-body">
                    Panel content
                </div>
            </div>
            <div class="panel panel-danger">
                <div class="panel-heading">Panel heading without title</div>
                <div class="panel-body">
                    Panel content
                </div>
            </div>
        </div>
        <div class="col-md-6">
            <div class="article_list">
                {% for article in article_list %}
                    <div class="article-item">
                        <h5><a href="">{{ article.title }}</a></h5>
                        <div class="article-desc">
                            <span class="media-left">
                                <a href=""><img height="56" width="56" src="media/{{ article.user.avatar }}" alt=""></a>
                            </span>
                            <span class="media-right">
                                {{ article.desc }}
                            </span>
                        </div>
                        <div class="small pub_info">
                            <span><a href="">{{ article.user.username }}</a> </span>    
                            <span>发布于  {{ article.create_time|date:'Y-m-d:H:i' }}</span>   
                            <span class="glyphicon glyphicon-comment"></span>评论({{ article.comment_count }})  
                            <span class="glyphicon glyphicon-thumbs-up"></span>点赞({{ article.up_count }})
                        </div>
                    </div>
                        <hr>
                {% endfor %}
            </div>
        </div>
        <div class="col-md-3">
            <div class="panel panel-default">
                <div class="panel-heading">Panel heading without title</div>
                <div class="panel-body">
                    Panel content
                </div>
            </div>
            <div class="panel panel-primary">
                <div class="panel-heading">Panel heading without title</div>
                <div class="panel-body">
                    Panel content
                </div>
            </div>
        </div>
    </div>
</div>
 
</body>
</html>

头像设置

点击头像===点击input(这里使用label标签属性方法)

  首先,我们下载一个默认头像:

 注册头像的预览方法

  ?1,获取用户选中的问卷对象

   2,获取文件对象的路径

   3,修改img的src,src=文件路径对象

 取用户的标签,基于AJAX提交formdata数据

// 基于AJAX 提交数据
    $(".reg_btn").click(function () {
 
        var formdata = new FormData();
        formdata.append('user', $("#id_user").val());
        formdata.append('pwd', $("#id_pwd").val());
        formdata.append('re_pwd', $("#id_re_pwd").val());
        formdata.append('email', $("#id_email").val());
        formdata.append('avatar', $("#avatar")[0].files[0]);
        formdata.append('csrfmiddlewaretoken', $('[name= "csrfmiddlewaretoken"]').val());
 
        $.ajax({
            url:"",
            type:"post",
            data: formdata,
            processData:false,
            contentType:false,
            success:function (data) {
                console.log(data)
            }
        })
    })

AJAX在注册页面显示错误信息?

views: form.errors?

  Ajax.success方法? data.msg 就是上面的errors

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
 
    <link rel="stylesheet" href="/static/blog/bootstrap-3.3.7-dist/css/bootstrap.css">
    <style>
        #avatar_image{
            margin-left: 20px;
        }
        #avatar{
            display: none;
        }
        .error{
            color: red;
        }
    </style>
 
</head>
<body>
<h3 class=" text-center">注册页面</h3>
<div class="container">
    <div class="row">
        <div class="col-md-6 col-lg-offset-3">
            <form>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
{#                        <label for="user">{{ field.label }}</label>#}
                        <label for={{ field.auto_id }}>{{ field.label }}</label>
                        {{ field }}<span class="error pull-right"></span>
                    </div>
                {% endfor %}
 
                <div class="form-group">
                    <label for="avatar">
                        头像
                        <img id="avatar_image" src="/static/img/default.jpg" alt="" width="60" height="60">
                    </label>
 
                    <input type="file" id="avatar">
                </div>
 
                <input type="button" value="Submit" class="btn btn-default reg_btn">
 
            </form>
        </div>
    </div>
</div>
 
</body>
<script src="/static/JS/jquery-3.2.1.js"></script>
<script>
 
    // 基于AJAX 提交数据
    $(".reg_btn").click(function () {
        console.log($("#form").serializeArray());
 
        var request_data = $("#form").serializeArray();
        $.each(request_data, function (index, data) {
           formdata.append(data.name, data.value)
        });
 
        var formdata = new FormData();
 
        formdata.append('avatar', $("#avatar")[0].files[0]);
        formdata.append('csrfmiddlewaretoken', $('[name= "csrfmiddlewaretoken"]').val());
 
        $.ajax({
            url:"",
            type:"post",
            data: formdata,
            processData:false,
            contentType:false,
            success:function (data) {
                console.log(data);
                if(data.user){
                    //  注册成功
                }else{
                    // 注册失败
                    console.log(data.msg);
                    $.each(data.msg, function (field, error_list){
                        console.log(field, error_list);
                        $("#id_" +field).next().html(error_list[0])
                    })
                }
            }
        })
    })
</script>
 
</html>

当我们用户输入后,需要清空用户名的错误信息。

??使用Admin 去录入数据

(关于Django admin的详细内容,我们后面补充)

  这里我们基于admin 去录入文章数据。

  为了让admin界面管理我们的数据模型,我们需要先注册数据模型到admin。所以我们去 admin.py 中注册模型,代码如下:

from django.contrib import admin
 
# Register your models here.
from blog import models
 
admin.site.register(models.UserInfo)
admin.site.register(models.Blog)
admin.site.register(models.Category)
admin.site.register(models.Tag)
admin.site.register(models.Article)
admin.site.register(models.ArticleUpDown)
admin.site.register(models.Article2Tag)
admin.site.register(models.Comment)

注册后,我们需要通过下面命令来创建超级用户:

python manage.py createsuperuser

  然后登陆Django的后台(http://127.0.0.1:8000/admin/),我们输入超级用户,进去如下:

  然后我们就可以录入数据了。

登陆页面

def login(request):
    '''
    登录视图函数:
        get请求响应页面
        post(Ajax)请求响应字典
    :param request:
    :return:
    '''
    if request.method == 'POST':
 
        response = {'user': None, 'msg': None}
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        valid_code = request.POST.get('valid_code')
 
        valid_code_str = request.session.get('valid_code_str')
        if valid_code.upper() == valid_code_str.upper():
            user = auth.authenticate(username=user, password=pwd)
            if user:
                # request.user == 当前登录对象
                auth.login(request, user)
                response['user'] = user.username
            else:
                response['msg'] = '用户名或者密码错误!'
        else:
            # 校验失败了
            response['msg'] = 'valid code error!'
 
        return JsonResponse(response)
 
    return render(request, 'login.html')

  login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.min.css">
 
</head>
<body>
 
<h3 class="text-center">登录页面</h3>
<div class="container">
    <div class="row">
        <div class="col-md-6 col-lg-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_image/" 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-success pull-right">注册</a>
 
            </form>
        </div>
    </div>
</div>
 
<script src="/static/JS/jquery-3.2.1.min.js"></script>
<script>
    // 刷新验证码
    $("#valid_code_img").click(function () {
        $(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) {
                    if (location.search){
                        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("");
                    },1000)
 
                }
            }
    })
</script>
 
</body>
</html>

个人站点页面的设计

1.我的标签,随机分类,标签列表
    随机分类:     /username/category/
    我的标签:     /username/tag/
    随笔归档:     /username/archive/
    
?
2.模板继承
    {% extends 'base.html' %}
?
    {% block content %}
    {% endblock content%}}
?
3.自定义标签
    /blog/templatetags/my_tag.py
?
    @register.inclusion_tag('classification.html')
    def get_classification_style(username):
        ...
        return {} # 去渲染 menu.html
?
4.分组查询 .annotate() / extra()应用
    多表分组
        tag_list = Tag.objects.filter(blog=blog).annotate(
            count = Count('article')).values_list('title', 'count')
?
    单表分组 / DATE_FORMAT() /  extra()
        date_list = Article.objects.filter(user=user).extra(
            select={"create_ym": "DATE_FORMAT(create_time,'%%Y-%%m')"}).values('create_ym').annotate(
            c = Count('nid')).values_list('create_ym', 'c')
?
5. 时间、区域配置
     TIME_ZONE = 'Asia/Shanghai'
     USE_TZ = False

个人站点设计的总体代码展示

views.py

def home_site(request, username, **kwargs):
    '''
    个人站点视图函数
    :param request:
    :return:
    '''
    print("执行的是home_site的内容")
    print('username:', username)
    user = UserInfo.objects.filter(username=username).first()
    #  判断用户是否存在
    if not user:
        return render(request, 'not_found.html')
 
    # 当用户存在的话 当前用户或者当前站点对应所有文章取出来
    # 1, 查询当前站点
    blog = user.blog
 
    # kwargs是为了区分访问的是站点页面还是站点下的跳转页面
    article_list = models.Article.objects.filter(user=user)
 
    if kwargs:
        condition = kwargs.get('condition')
        param = kwargs.get('param')
        print(condition)
        print(param)
        if condition == 'category':
            print(1)
            article_list = article_list.filter(category__title=param)
        elif condition == 'tag':
            print(2)
            article_list = article_list.filter(tags__title=param)
        else:
            print(3)
            year, month, day = 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, })

  home_site.html

{% extends 'base.html' %}
 
 
{% block content %}
 <div class="article_list">
    {% for article in article_list %}
        <div class="article-item clearfix">
            <h5><a href="/{{ article.user.username }}/articles/{{ article.pk }}">{{ article.title }}</a></h5>
            <div class="article-desc">
                {{ article.desc }}
            </div>
            <div class="small pub_info pull-right">
                <span>发布于   {{ article.create_time|date:"Y-m-d H:i" }}</span>  
                <span class="glyphicon glyphicon-comment"></span>评论({{ article.comment_count }})  
                <span class="glyphicon glyphicon-thumbs-up"></span>点赞({{ article.up_count }})  
            </div>
        </div>
        <hr>
    {% endfor %}
 
</div>
{% endblock %}


?个人站点页面的文章查询

当博客园用户站点不存在的时候,我们发现,会返回一个下面页面:

 当然,我们也可以做与上面一样的页面,其代码入下:

  not_found.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Error_404_资源不存在</title>
 
    <link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.css">
 
    <style type="text/css">
        body{
            margin:8% auto 0;
            max-width: 550px;
            min-height: 200px;
            padding:10px;
            font-family: Verdana,Arial,Helvetica,sans-serif;
            font-size:14px;
            }
        p{
            color:#555;
            margin:10px 10px;
            }
        img {
            border:0px;
            }
        .d{
            color:#404040;
            }
 
    </style>
 
</head>
<body>
 
<div class="container" style="margin-top: 100px">
    <div class="text-center">
        <a href="">
            <img src="/static/img/logo_small.gif" alt="">
        </a>
        <p>
            <b>404.</b>
            抱歉!您访问的资源不存在!
        </p>
        <p class="d">
            请确认您输入的网址是否正确,如果问题持续存在,请发邮件至
            <b>contact@qq.com</b>
            与我们联系。
        </p>
        <p>
            <a href="http://www.baidu.com/">返回百度查询</a>
        </p>
    </div>
</div>
 
</body>
</html>

个人站点页面的日期查询

?

?如何只拿出来 年和月?

?

Extra函数的学习

?  Django对一些复杂的函数不能一一对应,所以提供了一种extra函数。

跳转过滤功能的实现

 views.py (home_site函数)

article_list = models.Article.objects.filter(user=user)
if kwargs:
    condition = kwargs.get('condition')
    param = kwargs.get('param')
    print(condition)
    print(param)
    if condition == 'category':
        print(1)
        article_list = article_list.filter(category__title=param)
    elif condition == 'tag':
        print(2)
        article_list = article_list.filter(tags__title=param)
    else:
        print(3)
        year, month, day = param.split("-")
        article_list = article_list.filter(create_time__year=year, create_time__month=month)

 home_site.html

<div class="col-md-3">
            <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 data in data_list %}
                        <p><a href="/{{ username }}/archive/{{ data.0 }}" >{{ data.0 }}({{ data.1 }})</a></p>
                    {% endfor %}
                </div>
            </div>
        </div>

?文章详细页的设计

1.文章详情页的设计

2.文章详情页的数据构建

3.文章详情页点赞样式的完成(基本仿照博客园)

4.文章评论样式的添加(基本仿照博客园)

5.文章评论树的添加(支持对对评论的评论)

6.文章评论中邮件发送

总体的代码及其样式展示

 views.py

def get_classification_data(username):
    user = 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")
 
    tag_list = models.Tag.objects.filter(blog=blog).values("pk").annotate(c=Count("article")).values_list("title", 'c')
 
    data_list = models.Article.objects.filter(user=user).extra(
        select={"y_m_date": "date_format(create_time, '%%Y-%%m-%%d')"}).values(
        'y_m_date').annotate(c=Count('nid')).values_list('y_m_date', 'c')
 
    return {"blog": blog, 'cate_list': cate_list, 'tag_list': tag_list, 'data_list': data_list}
 
 
def article_detail(request, username, article_id):
    print("执行的是article_detail的内容")
 
    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())

article_detail.html


        <div class="comments list-group">
            <p class="tree_btn">评论树</p>
            <div class="comment_tree">
 
 
            </div>
 
            <script>
 
                 $.ajax({
                        url: "/get_comment_tree/",
                        type: "get",
                        data: {
                            article_id: "{{ article_obj.pk }}"
                        },
                        success: function (comment_list) {
                            console.log(comment_list);
 
                            $.each(comment_list, function (index, comment_object) {
 
                                var pk = comment_object.pk;
                                var content = comment_object.content;
                                var parent_comment_id = comment_object.parent_comment_id;
                                var s = '<div class="comment_item" comment_id=' + pk + '><span>' + content + '</span></div>';
 
                                if (!parent_comment_id) {
 
                                    $(".comment_tree").append(s);
                                } else {
 
                                    $("[comment_id=" + parent_comment_id + "]").append(s);
                                }
                            })
 
                        }
                    })
 
            </script>
 
 
            <p>评论列表</p>
 
            <ul class="list-group comment_list">
 
                {% for comment in comment_list %}
                    <li class="list-group-item">
                        <div>
                            <a href=""># {{ forloop.counter }}楼</a>   
                            <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>  
                            <a href=""><span>{{ comment.user.username }}</span></a>
                            <a class="pull-right reply_btn" username="{{ comment.user.username }}"
                               comment_pk="{{ comment.pk }}">回复</a>
                        </div>
 
                        {% if comment.parent_comment_id %}
                            <div class="pid_info well">
                                <p>
                                    {{ comment.parent_comment.user.username }}: {{ comment.parent_comment.content }}
                                </p>
                            </div>
                        {% endif %}
 
                        <div class="comment_con">
                            <p>{{ comment.content }}</p>
                        </div>
 
                    </li>
                {% endfor %}
 
 
            </ul>
 
            <p>发表评论</p>
            <p>昵称:<input type="text" id="tbCommentAuthor" class="author" disabled="disabled" size="50"
                         value="{{ request.user.username }}">
            </p>
            <p>评论内容:</p>
            <textarea name="" id="comment_content" cols="60" rows="10"></textarea>
            <p>
                <button class="btn btn-default comment_btn">提交评论</button>
            </p>
        </div>
        <script>
            // 点赞请求
            $("#div_digg .action").click(function () {
                var is_up = $(this).hasClass("diggit");
 
 
                $obj = $(this).children("span");
 
                $.ajax({
                    url: "/digg/",
                    type: "post",
                    data: {
                        "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(),
                        "is_up": is_up,
                        "article_id": "{{ article_obj.pk }}",
                    },
                    success: function (data) {
                        console.log(data);
 
                        if (data.state) {
                            var val = parseInt($obj.text());
                            $obj.text(val + 1);
                        }
                        else {
                            var val = data.handled ? "您已经推荐过!" : "您已经反对过!";
                            $("#digg_tips").html(val);
 
                            setTimeout(function () {
                                $("#digg_tips").html("")
                            }, 1000)
 
                        }
 
                    }
                })
 
            });
 
            // 评论请求
            var pid = "";
 
            $(".comment_btn").click(function () {
 
                var content = $("#comment_content").val();
 
                if (pid) {
                    var index = content.indexOf("\n");
                    content = content.slice(index + 1)
                }
 
 
                $.ajax({
                    url: "/comment/",
                    type: "post",
                    data: {
                        "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(),
                        "article_id": "{{ article_obj.pk }}",
                        "content": content,
                        pid: pid
                    },
                    success: function (data) {
 
                        console.log(data);
 
                        var create_time = data.create_time;
                        var username = data.username;
                        var content = data.content;
 
                        var s = `
                           <li class="list-group-item">
                              <div>
 
                                  <span>${create_time}</span>  
                                  <a href=""><span>${username}</span></a>
 
                              </div>
                              <div class="comment_con">
                                  <p>${content}</p>
                              </div>
 
                            </li>`;
 
                        $("ul.comment_list").append(s);
 
                        // 清空评论框
                        pid = "",
                                $("#comment_content").val("");
 
                    }
                })
 
 
            });
 
            // 回复按钮事件
 
            $(".reply_btn").click(function () {
 
                $('#comment_content').focus();
                var val = "@" + $(this).attr("username") + "\n";
                $('#comment_content').val(val);
 
 
                pid = $(this).attr("comment_pk");
 
 
            })
        </script>
 
    </div>
{% endblock %}

?点赞,评论,评论树及其发送邮件

根评论:对文章的评论

子评论:对评论的评论

区别:是否有父评论

评论:? ?1.构建样式

    2.提交根评论

    3.显示跟评论——render显示? ?——AJAX显示

    4.提交子评论

    5.显示子评论——render显示? ?——AJAX显示

    6.评论树的显示

def digg(request):
    """
    点赞功能
    :param request:
    :return:
    """
    print(request.POST)
 
    article_id = request.POST.get("article_id")
    is_up = json.loads(request.POST.get("is_up"))  # "true"
    # 点赞人即当前登录人
    user_id = request.user.pk
    obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
 
    response = {"state": True}
    if not obj:
        ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up)
 
        queryset = models.Article.objects.filter(pk=article_id)
        if is_up:
            queryset.update(up_count=F("up_count") + 1)
        else:
            queryset.update(down_count=F("down_count") + 1)
    else:
        response["state"] = False
        response["handled"] = obj.is_up
 
    return JsonResponse(response)
 
 
def comment(request):
    """
    提交评论视图函数
    功能:
    1 保存评论
    2 创建事务
    3 发送邮件
    :param request:
    :return:
    """
    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)
 
    response = {}
 
    response["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")
    response["username"] = request.user.username
    response["content"] = content
 
    # 发送邮件
 
    from django.core.mail import send_mail
    from cnblog import settings
 
    # send_mail(
    #     "您的文章%s新增了一条评论内容"%article_obj.title,
    #     content,
    #     settings.EMAIL_HOST_USER,
    #     ["916852314@qq.com"]
    # )
    ...
    import threading
 
    t = threading.Thread(target=send_mail, args=("您的文章%s新增了一条评论内容" % article_obj.title,
                                                 content,
                                                 settings.EMAIL_HOST_USER,
                                                 ["916852314@qq.com"])
                         )
    t.start()
   ...
    return JsonResponse(response)
 
 
def get_comment_tree(request):
    article_id = request.GET.get("article_id")
    response = list(models.Comment.objects.filter(article_id=article_id).order_by("pk").values("pk", "content",
                                                                                               "parent_comment_id"))
 
    return JsonResponse(response, safe=False)

  点赞的jQuery代码展示:

$("#div_digg .action").click(function () {
              var is_up = $(this).hasClass("diggit");
 
                $obj = $(this).children('span');
                $.ajax({
                    url: '/digg/',
                    type: 'post',
                    data: {
                        "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(),
                        "is_up": is_up,
                        "article_id": "{{ article_obj.pk }}",
 
                    },
                    success:function (data) {
                        //alert(is_up);
                        console.log(data);
                        if (data.state){
                            var val = parseInt($obj.text());
                            $obj.text(val+1);
                            {#if (is_up){#}
                            {#    var val=parseInt($("#digg_count").text());#}
                            {#    $("#digg_count").text(val+1);#}
                            //}
                            {#else{#}
                            {#    var val=parseInt($("#bury_count").text());#}
                            {#    $("#bury_count").text(val+1);#}
                            //}
                        }else {
                            var val = data.handled?"您已经推荐过!":"您已经反对过!";
                            $("#digg_tips").html(val);
                            {#if (data.handled){#}
                            {#    $("#digg_tips").html("您已经推荐过!")#}
                            //}else {
                            {#    $("#digg_tips").html("您已经反对过!")#}
                            //}
 
                            setTimeout(function () {
                                $("#digg_tips").html()
                            }, 2000)
 
                        }
                    }
                })
            });

render显示根评论:

?

?评论树的显示

  (我们需要将评论楼改为评论树展开)

后台管理页面设计

1.支持文章编辑

2.支持富文本编辑器(支持渲染已有文章,并支持文本编辑器的上传功能)

3.支持删除文章(未添加,很简单,可自行添加)

4.防止Xss攻击(基于BS4)

views.py

@login_required
def cn_backend(request):
    article_list = models.Article.objects.filter(user=request.user)
 
    return render(request, 'backend/backend.html', locals())
 
 
from bs4 import BeautifulSoup
 
 
@login_required
def add_article(request):
    if request.method == 'POST':
        title = request.POST.get("title")
        content = request.POST.get("content")
 
        # 防止XSS攻击,过滤script
        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)
        return redirect('/cn_backend/')
    return render(request, "backend/add_article.html")
 
 
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)
 
    response = {
        'error': 0,
        'url': '/media/add_article_img/%s' % img_obj.name
    }
    import json
    return HttpResponse(json.dumps(response))

 add_articles.html

{% 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/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 %}

?

小知识点补充

on_delete
当删除关联表中的数据时,当前表与其关联的行的行为。

models.CASCADE
  删除关联数据,与之关联也删除

models.DO_NOTHING
  删除关联数据,引发错误IntegrityError

models.PROTECT
  删除关联数据,引发错误ProtectedError

models.SET_NULL
  删除关联数据,与之关联的值设置为null(前提FK字段需要设置为可空)

models.SET_DEFAULT
  删除关联数据,与之关联的值设置为默认值(前提FK字段需要设置默认值)

OneToOneField就是ForeignKey + unique
?

字段类的属性(字段类型)


max_length (最大长度)
null=True (可以为空)
default=‘’ (设置默认值)
unique=True (数据值必须唯一)
db_index=True (设置索引)
verbose_name=‘’ (注释)
db_constraint=False (数据约束 放在ForeignKey中,不建立外键关联 可以使用正反向查询 可能存在脏数据 可在代码层面进行限制)

ManyToManyField

自动创建第三张表
手动创建第三张表(当中间表除了关联字段外还需其他字段)
手动创建第三章关系表
字段类属性中增加:through= 通过哪张表进行关联 througu_fields= 设置关联的字段
OneToOneField,ForeignKey,ManyToManyField

related_name:反向操作时,使用的字段名,用于代替原反向查询的’表名_set‘。
related_query_name:反向操作时,使用的连接的前缀,用户替换表名。
?

终端执行数据库迁移命令

python38 manage.py makemigretions
python38 manage.py migrater

没有安装mysqlclient会报错

解决方案一:
在任意的双下init文件中编写以下代码:

import pymysql
pymysql.install_as_MySQLdb()

在django2.0.7及以后版本,需要改源码才能使用,operations.py中的146行,改成query = query.encode(errors=‘replace’)。

解决方法二:

pip3 instasll mysqlclient

参考博客:Django学习笔记(18)——BBS+Blog项目开发(2)主体思路及流程 - 战争热诚 - 博客园

  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2022-09-21 00:24:34  更:2022-09-21 00:24:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/26 14:27:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计