BBS项目

BBS项目

文章目录

一、项目开发流程

1 项目分类
	-针对互联网用户:抖音,淘宝   sanic
    	-产品经理
    -公司内部,给用户定制软件
2 项目开发模式分类
	-瀑布开发模式
    -敏捷开发:devops  ci cd
    
3 项目开发流程
	-立项
    -需求分析(产品经理提,用户提的需求)
    -原型图(流程图,产品经理设计)
    -美工切图
    -技术选型,数据库架构设计
    -前后台开发(开发:协同开发:git)
    -开发完成---运维上线---联调(测试环境)
    -测试测试
    -修改bug(开发)
    -上线运行
    
    -迭代更新(开发)

4 多人博客项目(仿cnblogs),功能,需求(前后端混合)
	-注册(froms,ajax提交,上传头像)
    -登录功能(ajax提交,错误信息渲染)
    -首页(列出所有文章,作者头像,发布时间,点赞数。。广告位,轮播图)
    -个人站点(左侧过滤,inclusion_tag)
    -文章展示
    -点赞,点踩功能
    -评论功能
    -后台管理:展示我的所有文章
    -文章新增(修改,删除),防止xss攻击
    -修改密码,头像,个人信息(不讲)
    

二、项目需求

  • 多人博客(多用户,每个用户可以有自己的个人站点,编写自己的博客)
  • 登录注册(登录有图片验证码,注册可以上传用户自定义头像)
  • 登录注册用form组件进行校验和渲染页面,Ajax提交请求
  • 每个用户都有个人站点(可以根据时间、分类、标签来过滤文章)
  • 每个用户拥有后台管理(可以对文章、标签、分类进行增、删、改、查,新增文章使用富文本编辑器,处理XSS攻击)
  • 每个用户有个人站点样式、修改头像、修改密码
  • 1篇文章可以有多个标签,但是只能拥有1个分类(不支持属于多个分类,但是不限制于标签)
  • 在首页显示文章的用户头像、文章标题、文章摘要、发布时间、点赞数、评论数
  • 每个已登录的用户都可以对文章进行点赞点踩(只能点其中1个,点后无法继续点击,无法撤销)
  • 每篇文章都可以进行评论,每个评论可以有子评论(子评论也可以有子评论,以此类推)

设计程序

  • 设计程序(django2.2.2+mysql5.7)

三、BBS表分析

前言: 一个项目中最最最重要的不是业务逻辑的书写, 而是前期的表设计,只要将表设计好了,后续的功能书写才会一帆风顺

  • 7张物理表+一张虚拟表(Article2Tag)
# 数据库选择: 由于django自带的sqlite数据库对日期不敏感,所以我们换成MySQL

# 提示:
    1. 以下最好加一个verbose_name,
    2. 对于用户不想输入的数据可以设置为null=True,
    3. 针对于外键的指定, 为了能不考虑录入数据的顺序更高效的录入, 推荐指定null=True

# 创建书写顺序: 先建立表及普通字段. 最后建立外键关系

# 用户表     UserInfo
    继承: AbstractUser
    拓展字段:
        avatar          FileField(upload_to='', default='')
        phone           BigIntegerField
        gender          IntegerField(choices=gender_choices)
        create_time     DateTimeField(auto_null_add=True)

        # 一对一个人站点
        blog             OneToOneField(to='Blog')


# 个人站点表  Blog
    site_name     CharField
    site_title    CharField
    site_theme    CharField  存放js/css文件路径

# 文章分类表  Category
    name          CharField

    # 一对多个人站点
    blog          ForeignKey(to='Blog')

# 文章标签表  Tag
    name          CharField

    # 一对多个人站点
    blog          ForeignKey(to='Blog')

# 文章表     Article
    title         CharField
    desc          CharField
    content       TextField
    update_time   DateTimeField(auto_null=True)

    # 数据库设计优化普通字段. (注意: 默认值都是0)
    '''
    虽然下述的三个字段可以从其他表里面跨表查询计算得出,但是频繁跨表效率,
    因此我们建立普通字段, 在点赞点菜表 或者 文章评论表所对应在同一篇文章下新增了内容, 就对对应的普通字段进行自动的自增 或者 自减操作
    只需要确保后续在操作点赞点踩和评论表的时候同步的将对应的普通字段更新即可
    '''
    up_num        BigIntegerField(default=0)
    down_num      BigIntegerField(default=0)
    comment_num   BigIntegerField(default=0)

    # 一对多个人站点
    blog          ForeignKey(to='Blog')
    # 一对多用户表
    category      ForeignKey(to='Category')
    # 多对多文章标签
    tags          ManyToManyField(to='Tag', through='Article2Tag', through_fields=('article', 'tag'))

# 新建半自动多对多表: 文章和文章标签表 Article2Tag
    # 一对多文章
    article       ForeignKey(to='Article')
    # 一对多文章标签
    tag           ForeignKey(to='Tag')

# 点赞点踩表  UpAndDown
    # 一对多用户表
    user          ForeignKey(to='UserInfo')
    # 一对多文章表
    article       ForeignKey(to='Article')
    is_up         BooleanField()

# 文章评论表  Comment
    # 一对多用户表
    user          ForeignKey(to='UserInfo')
    # 一对多文章表
    article       ForeignKey(to='Article')

    # 根评论子评论概念: 根评论就是直接评论当前发布地内容, 子评论就是评论根评论的内容. 如下例子所示:
        1.PHP是世界上最牛逼的语言
            1.1 python才是最牛逼的
                1.2 java才是

    # 一对多自己: 根评论子评论.
    
    # 自关联的第一种写法:
    parent        ForeignKey(to='Comment', null=True)  
    # 自关联的第二种写法: 使用ORM专门提供的自关联写法
    parent        ForeignKey(to='self', null=True)

    id     user_id    article_id     parent_id
     1        1            1
     2        2            1            1
    描述: 这里表示的是用户2是评论是评论用户1. 用户1是根评论. 根评论为空作为标记

表关系图

image-20210418195846037

四、项目配置

配置模板文件路径settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, "template")],  # template文件夹位置
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# 猴子补丁
import pymysql

pymysql.install_as_MySQLdb()

数据库配置

手动创建数据库

create database cnblogs;

image-20210418201934245

Django修改数据库,这里我们使用MySQL,在setting.py文件中配置

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'bbs',    # 数据库名
        'HOST': '127.0.0.1',    # 主机IP(本地为127.0.0.1)
        'PORT': 3306,   # MySQL端口号:默认3306
        'USER':  'root',  # 数据库用户名
        'PASSWORD': '123456', # 数据库密码
    }
}

创建超级用户

python manage.py createsuperuser

运行项目

python manage.py runserver

五、数据库创建与同步

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 数据库中该字段可以为空
    blank=True  admin后台管理字段可以为空
    '''

    # 头像
    avatar = models.FileField(upload_to='static/img/avatar/', default='static/img/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.DO_NOTHING)

    class Meta:
        verbose_name_plural = '用户表'  # 修改admin后台管理默认的表名
        # verbose_name = '用户表'  #末尾还是会自动加s

    def __str__(self):
        return self.username


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)
    is_email =models.BooleanField(default=False)
    class Meta:
        verbose_name_plural = "个人站点表"

    def __str__(self):
        return self.site_name


class Category(models.Model):
    name = models.CharField(verbose_name='文章分类', max_length=32)

    # 一对多个人站点表
    blog = models.ForeignKey(to='Blog', null=True, on_delete=models.DO_NOTHING)

    class Meta:
        verbose_name_plural = '文章分类'

    def __str__(self):
        return self.name


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

    class Meta:
        verbose_name_plural = '文章标签表'

    def __str__(self):
        return self.name


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.BigIntegerField(verbose_name='点赞数', default=0)
    down_num = models.BigIntegerField(verbose_name='点踩数', default=0)
    comment_num = models.BigIntegerField(verbose_name='评论数', default=0)

    # 外键字段
    # 一对多个人站点
    blog = models.ForeignKey(to="blog", null=True, on_delete=models.DO_NOTHING)
    # 一对多文章分类
    category = models.ForeignKey(to='Category', null=True, on_delete=models.DO_NOTHING)

    # 多对多文章标签
    tag = models.ManyToManyField(to='Tag',
                                 through='Article2Tag',
                                 through_fields=('article', 'tag')
                                 )

    class Meta:
        verbose_name_plural = "文章表"

    def __str__(self):
        try:
            return str(self.id) + '----' + self.title + '-------' + self.blog.site_title
        except:
            return self.title


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

    class Meta:
        verbose_name_plural = '文章与标签表'


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

    class Meta:
        verbose_name_plural = '点赞点踩表'


class Comment(models.Model):
    user = models.ForeignKey(to='UserInfo', on_delete=models.DO_NOTHING)
    article = models.ForeignKey(to='Article', on_delete=models.DO_NOTHING)
    content = models.CharField(verbose_name='评论内容', max_length=255)
    comment_time = models.DateTimeField(verbose_name='评论时间',auto_now=True, max_length=255)

    # 自关联,根评论子评论,一对多子评论
    parent = models.ForeignKey(to='self', null=True, on_delete=models.DO_NOTHING)  # 有些评论就是根评论

    class Meta:
        verbose_name_plural = '文章评论表'

执行迁移命令

python manage.py makemigrations
python manage.py migrate

六、注册功能

注册forms类编写

用froms组件解耦合

'''
1. 项目只用到一个forms组件: 直接新建.py文件存放
2. 项目中用到多个forms组件: 新建文件夹, 再建不同的.py文件
    self_forms
        register_forms.py
        login_forms.py
        ...
'''

# 针对用户表的forms组件代码
from django import forms
from blog import models


class MyRegForm(forms.Form):
    username = forms.CharField(label='用户名', min_length=3, max_length=8,
                               error_messages={
                                   'required': '用户名不能为空',
                                   'min_length': '用户名最少3位',
                                   'max_length': '用户名最大8位'
                               },
                               # 还需要让标签有样式
                               widget=forms.widgets.TextInput(attrs={'class': 'form-control'})
                               )
    password = forms.CharField(label='密码', min_length=3, max_length=8,
                               error_messages={
                                   'required': '密码不能为空',
                                   'min_length': '密码最少3位',
                                   'max_length': '密码最大8位'
                               },
                               # 还需要让标签有样式
                               widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
                               )
    confirm_password = forms.CharField(label='确认密码', min_length=3, max_length=8,
                                       error_messages={
                                           'required': '确认密码不能为空',
                                           'min_length': '确认密码最少3位',
                                           'max_length': '确认密码最大8位'
                                       },
                                       # 还需要让标签有样式
                                       widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
                                       )
    email = forms.EmailField(label='邮箱',
                             error_messages={
                                 'required': '邮箱不能为空',
                                 'invalid': '邮箱格式不正确',
                             },
                             widget=forms.widgets.EmailInput(attrs={'class': 'form-control'})
                             )
    blog =forms.CharField

    # 钩子函数
    # 局部钩子:校验用户名是否已经存在
    def clean_username(self):
        username = self.cleaned_data.get('username')
        # 去数据库中校验
        is_exist = models.UserInfo.objects.filter(username=username)
        if is_exist:
            # 提示信息
            self.add_error('username', '用户名已存在')
        return username

    # 全局钩子:校验两次是否一致
    def clean(self):
        password = self.cleaned_data.get('password')
        confirm_password = self.cleaned_data.get('confirm_password')
        if not password == confirm_password:
            self.add_error('password', '两次密码不一致')
        return self.cleaned_data

注册功能前端页面搭建

<!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">
    <script src="/static/jquery-3.3.1.js"></script>
</head>
<body>
<div class="container-fluid">
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <div class="panel panel-primary">
                <div class="panel-heading">
                    <h3 class="panel-title">注册功能</h3>
                </div>
                <div class="panel-body">
                    <form id="my_form">
                        {% for item in form %}
                            <div class="form-group">
                                <label for="{{ item.auto_id }}">{{ item.label }}</label>
                                {{ item }}
                                <span></span>
                            </div>
                        {% endfor %}
                        <div class="form-group">
                            <label for="myfile">头像
                                <img src="/static/img/default.png" id='id_img' width="80px" height="80px" style="margin-left: 20px">
                            </label>

                            <input type="file" id="myfile" style="display:none;">

                        </div>
                        <div class="text-center">
                            <input type="button" value="注册" class="btn btn-warning">
                        </div>

                    </form>


                </div>
            </div>

        </div>
    </div>
</div>
</body>
<script>
    $('#myfile').change(function () {
        //取到文件
        let myfile=$(this)[0].files[0]
        //let myfile=$('#myfile')
        //借助文件阅读器对象,把文件读到这个对象中
        let filereader=new FileReader()
        filereader.readAsDataURL(myfile)

        //等待读完,把它放到img标签上
        filereader.onload=function () {
            $('#id_img').attr('src',filereader.result)
        }

        //$('#id_img').attr('src','https://upload-images.jianshu.io/upload_images/9522239-c31b38a1a9913ac4.png?imageMogr2/auto-orient/strip|imageView2/2/w/202/format/webp')

    })


</script>
</html>

头像实时显示

<script>
    $('#myfile').change(function () {
        //取到文件
        let myfile=$(this)[0].files[0]
        //let myfile=$('#myfile')
        //借助文件阅读器对象,把文件读到这个对象中
        let filereader=new FileReader()
        filereader.readAsDataURL(myfile)

        //等待读完,把它放到img标签上
        filereader.onload=function () {
            $('#id_img').attr('src',filereader.result)
        }

        //$('#id_img').attr('src','https://upload-images.jianshu.io/upload_images/9522239-c31b38a1a9913ac4.png?imageMogr2/auto-orient/strip|imageView2/2/w/202/format/webp')

    })


</script>

注册功能后端

def register(request):
    if request.method == 'GET':
        register_form = RegisterForm()
        return render(request, 'register.html', context={'form': register_form})
    elif request.method == 'POST':
        response = {'status': 100, 'msg': None}
        register_form = RegisterForm(request.POST)
        if register_form.is_valid():
            # 数据校验通过
            # 可能传头像,可能没传头像
            clean_data=register_form.cleaned_data
            print(clean_data)
            my_file=request.FILES.get('myfile')
            if my_file: # 传了头像
                # FileField字段类型直接接受一个文件对象,
                # 它会把文件存到upload_to='avatar/',然后把路径存到数据库中
                # 相当于with open 打开文件,把文件存到avatar路径下,把路径赋值给avatar这个字段
                clean_data['avatar']=my_file
            clean_data.pop('re_password')
            models.UserInfo.objects.create_user(**clean_data)
            response['msg']='恭喜你,注册成功'
            response['next_url']='/login/'

        else:
            response['status']=101
            response['msg'] = register_form.errors

        return JsonResponse(response)

注册功能整体代码

# 发送ajax请求,使用的Formdata
#form标签.serializeArray()

# 整体代码
$('#id_submit').click(function () {
        let formdata = new FormData()
        formdata.append('myfile', $('#myfile')[0].files[0])
        //方案一
        /*
        formdata.append('username',$('#id_username').val())
        formdata.append('password',$('#password').val())
        formdata.append('re_password',$('#id_re_password').val())
        formdata.append('email',$('#id_email').val())
         */

        //方案二
        let form_data = $('#my_form').serializeArray()
        //console.log(form_data)
        $.each(form_data, function (index, element) {
            //console.log(index)
            //console.log(element)
            formdata.append(element.name, element.value)
        })
        //console.log(formdata.get('username'))
        $.ajax({
            url: '/register/',
            method: 'post',
            contentType: false,
            processData: false,
            data: formdata,
            success: function (data) {
                console.log(data)
                if (data.status == 100) {
                    location.href = data.next_url
                    //location.href='/login/'
                } else {
                    $.each(data.msg, function (key, value) {
                        //console.log('#id_'+key)
                        if (key == '__all__') {
                            $('#id_error').html(value[0])
                        } else {
                            //取到input标签的下一个
                            //$('#id_'+key).next().html(value[0])
                            //链式调用
                            //$('#id_'+key).parent().addClass('has-error')
                            //链式调用
                            $('#id_' + key).next().html(value[0]).parent().addClass('has-error')

                        }

                    })

                    //加了一个定时器,3s以后干某些事
                    setTimeout(function () {
                        //清除红色框
                        $('.form-group').removeClass('has-error')
                        //清空所有错误信息
                        $('.error').html('')
                    }, 3000)
                }
            }
        })
    })

注册功能前端错误渲染

success: function (data) {
    console.log(data)
    if (data.status == 100) {
        location.href = data.next_url
    } else {
        $.each(data.msg, function (key, value) {
            if (key == '__all__') {
                $('#id_error').html(value[0])
            } else {
                $('#id_' + key).next().html(value[0]).parent().addClass('has-error')
            }
        })
        setTimeout(function () {
            //清除红色框
            $('.form-group').removeClass('has-error')
            //清空所有错误信息
            $('.error').html('')
                    }, 3000)
                }
}

七、登录功能

登录页面搭建

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
    <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.min.css">
    <script src="/static/jquery-3.3.1.js"></script>
</head>
<body>
<div class="container-fluid">
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <div class="panel panel-primary">
                <div class="panel-heading">
                    <h3 class="panel-title">登录功能</h3>
                </div>
                <div class="panel-body">
                    <form id="my_form">
                        {% csrf_token %}

                        <div class="form-group">
                            <label for="">用户名</label>
                            <input type="text" id="id_username" class="form-control">
                            <span class="danger pull-right error"></span>
                        </div>
                        <div class="form-group">
                            <label for="">密码</label>
                            <input type="text" id="id_password" class="form-control">
                            <span class="danger pull-right error"></span>
                        </div>

                        <div class="form-group">
                            <label for="">验证码</label>

                            <div class="row">
                                <div class="col-md-6">
                                    <input type="text" id="id_code" class="form-control">
                                </div>
                                <div class="col-md-6">

                                    <img src="/get_code/" alt="" height="35px" width="300px">
                                </div>
                            </div>

                        </div>


                        <div class="text-center">
                            <input type="button" value="登录" class="btn btn-warning" id="id_submit"><span
                                class="danger error"
                                id="id_error"
                                style="margin-left: 10px"></span>
                        </div>

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

        </div>
    </div>
</div>
</body>
</html>

手写验证码(第三方)

ttf字体网址: 点我点我

提示: 我们的计算机上面致所有能够输出各式各样的字体样式
内部其实对应的是一个个.ttf结尾的文件

验证码HTML代码书写
# img标签的src属性可以指定三种
    1. 图片路径
    2. url
    3. 图片的二进制数据. (提示: 我们这里使用的验证码图片就是使用这个特性)

# 关于scr路径的问题拓展: 
    如果你的网址路径默认是: http://127.0.0.1:8000/home
    如果你的src路径是:  static/img/default.png   那么你的路径就是:  http://127.0.0.1:8000/home/static/img/default.png
    如果你的src路径是:  /static/img/default.png  那么你的路径就是:  http://127.0.0.1:8000/static/img/default.png
                    

后端代码:

'''
1 生成验证码的模块
2 集成第三方,极验滑动验证,腾讯验证码(sdk)
3 自己写
'''

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


def get_code(request):
    # 最终方案
    # img = Image.new('RGB', (300, 30), get_random())
    img = Image.new('RGB', (250, 30), (250, 250, 250))
    # 第一个参数,是文字格式的文件,ttf格式,第二个参数是文字大小
    img_font = ImageFont.truetype('./static/font/ss.TTF', 20)
    # 拿到一个画板对象,把图片放到画板上,指定写的字的字体是什么
    img_draw = ImageDraw.Draw(img)
    # 在画板上写文字
    # 随机生成5位 小写字母,大写字母,和数字
    code = ''
    for i in range(5):
        low_char = chr(random.randint(97, 122))
        up_char = chr(random.randint(65, 90))
        number_char = str(random.randint(0, 9))
        res = random.choice([low_char, up_char, number_char])
        code += res
        img_draw.text((20 + i * 40, 0), res, fill=get_random(), font=img_font)
    print(code)
    request.session['code'] = code
    # 画点和线
    # 画线和点圈
    width = 250
    height = 30
    for i in range(5):
        x1 = random.randint(0, width)
        x2 = random.randint(0, width)
        y1 = random.randint(0, height)
        y2 = random.randint(0, height)
        # 在图片上画线
        img_draw.line((x1, y1, x2, y2), fill=get_random())

    for i in range(20):
        # 画点
        img_draw.point([random.randint(0, width), random.randint(0, height)], fill=get_random())
        x = random.randint(0, width)
        y = random.randint(0, height)
        # 画弧形
        img_draw.arc((x, y, x + 4, y + 4), 0, 90, fill=get_random())

    bytes_io = BytesIO()
    img.save(bytes_io, 'png')  # 写到内存中,需要传format,图片格式
    return HttpResponse(bytes_io.getvalue())  # 把内容读出来
验证码刷新
$('#id_img').click(function () {
        let img_url = $('#id_img')[0].src
        $('#id_img')[0].src = img_url + '?'
    })

登录功能前后端

前端
   $('#id_submit').click(function () {
        $.ajax({
            url: '/login/',
            method: 'post',
            data: {
                username: $('#id_username').val(),
                password: $('#id_password').val(),
                code: $('#id_code').val(),
                csrfmiddlewaretoken:'{{ csrf_token }}'
            },
            success:function (data) {
                if(data.status==100){
                    //location.href='/index/'
                    location.href=data.url
                }else{
                    $('#id_error').html(data.msg)
                }

            }
        })

    })
后端
def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    else:
        response = {'status': 100, 'msg': None}
        username = request.POST.get('username')
        password = request.POST.get('password')
        code = request.POST.get('code')
        if code.upper() == request.session.get('code').upper():
            user = authenticate(username=username, password=password)
            if user:
                auth.login(request,user)
                response['msg'] = '登录成功'
                response['url'] = '/index/'
            else:
                response['status'] = 102
                response['msg'] = '用户名或密码错误'
        else:
            response['status'] = 101
            response['msg'] = '验证码错误'
        return JsonResponse(response)

八、首页搭建

首页布局

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="/static/font-awesome-4.7.0/css/font-awesome.min.css">
    <script src="/static/jquery-3.3.1.js"></script>
    <script src="/static/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
    <style>
        .article_footer{
            margin-right: 10px;
        }
    </style>
</head>
<body>
<div class="head">
    <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>

                </ul>
                {% if request.user.is_authenticated %}
                    <ul class="nav navbar-nav navbar-right">
                        <li><a href="#">{{ request.user.username }}</a></li>
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                               aria-haspopup="true" aria-expanded="false">更多操作 <span class="caret"></span></a>
                            <ul class="dropdown-menu">
                                <li><a href="#">修改密码</a></li>
                                <li><a href="#">修改头像</a></li>
                                <li><a href="#">后台管理</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="/logout/">退出</a></li>
                            </ul>
                        </li>
                    </ul>
                {% else %}
                    <ul class="nav navbar-nav navbar-right">
                        <li><a href="/login/">登录</a></li>
                        <li><a href="/register/">注册</a></li>
                    </ul>
                {% endif %}

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


    <div class="row">
        <div class="col-md-2">
            <div class="panel panel-success">
                <div class="panel-heading">
                    <h3 class="panel-title">广告位招租</h3>
                </div>
                <div class="panel-body">
                    重金求子:<a href="">点我看美女</a>
                </div>
            </div>
            <div class="panel panel-info">
                <div class="panel-heading">
                    <h3 class="panel-title">老刘说书</h3>
                </div>
                <div class="panel-body">
                    <ul class="list-group">
                        <li class="list-group-item">金瓶梅</li>
                        <li class="list-group-item">西游记</li>
                        <li class="list-group-item">洪流吗</li>

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

        </div>
        <div class="col-md-7">
            <div>

                <div id="carousel-example-generic" class="carousel slide" data-ride="carousel">


                    <ol class="carousel-indicators">
                        <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
                        <li data-target="#carousel-example-generic" data-slide-to="1"></li>
                        <li data-target="#carousel-example-generic" data-slide-to="2"></li>
                    </ol>
                    <div class="carousel-inner" role="listbox">

                        {% for banner in banner_list %}
                            {% if forloop.first %}
                                <div class="item active">
                                    <img src="{{ banner.url }}" alt="...">
                                    <div class="carousel-caption">
                                        {{ banner.name }}
                                    </div>
                                </div>
                            {% else %}
                                <div class="item">
                                    <img src="{{ banner.url }}" alt="...">
                                    <div class="carousel-caption">
                                        {{ banner.name }}
                                    </div>
                                </div>
                            {% endif %}
                        {% endfor %}



                        <a class="left carousel-control" href="#carousel-example-generic" role="button"
                           data-slide="prev">
                            <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
                            <span class="sr-only">Previous</span>
                        </a>
                        <a class="right carousel-control" href="#carousel-example-generic" role="button"
                           data-slide="next">
                            <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
                            <span class="sr-only">Next</span>
                        </a>

                    </div>

                </div>


            </div>

            <div class="article" style="margin-top: 20px">
                {% for article in article_list %}
                    <div>
                        <h4 class="media-heading"><a href="">{{ article.title }}</a></h4>
                        <div class="media">
                            <div class="media-left media-middle">
                                <a href="#">
{#                                    <img class="media-object" src="media/{{ article.blog.userinfo.avatar }}" alt="..." height="60px" width="60px">#}
                                    <img class="media-object" src="https://pic.cnblogs.com/face/1921007/20200116181302.png" height="60px" width="60px">
                                </a>
                            </div>
                            <div class="media-body">
                                {{ article.desc }}

                            </div>
                            <div style="margin-top: 10px">
                                <span class="article_footer"><a href="">{{ article.blog.userinfo.username }}</a></span>
                                <span class="article_footer">{{ article.create_time|date:'Y-m-d H:i:s' }}</span>
{#                                <span class="glyphicon glyphicon-thumbs-up article_footer">&nbsp;{{ article.up_num }}</span>#}
                                <span class="article_footer"><i class="fa fa-comment-o fa-lg"></i>&nbsp;{{ article.up_num }}</span>
                                <span class="article_footer"><i class="fa fa-home fa-lg"></i>&nbsp;{{ article.down_num }}</span>
                                <span class='article_footer'><i class="fa fa-frown-o fa-lg"></i>&nbsp;{{ article.commit_num }}</span>
                            </div>
                        </div>
                        <hr>
                    </div>
                {% endfor %}


            </div>
        </div>

        <div class="col-md-3">

            <div class="panel panel-success">
                <div class="panel-heading">
                    <h3 class="panel-title">广告位招租</h3>
                </div>
                <div class="panel-body">
                    重金求子:<a href="">点我看美女</a>
                </div>
            </div>
            <div class="panel panel-info">
                <div class="panel-heading">
                    <h3 class="panel-title">老刘说书</h3>
                </div>
                <div class="panel-body">
                    <ul class="list-group">
                        <li class="list-group-item">金瓶梅</li>
                        <li class="list-group-item">西游记</li>
                        <li class="list-group-item">洪流吗</li>

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


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

</body>
</html>

首页文章个人头像显示

1 开启media
	-配置文件
    MEDIA_ROOT=os.path.join(BASE_DIR,'media')
    -路由
     re_path('^media/(?P<path>.*?)$', serve,kwargs={'document_root':settings.MEDIA_ROOT}),
    -前端:
    <img class="media-object" src="media/{{ article.blog.userinfo.avatar }}" alt="..." height="60px" width="60px">

修改密码

路由
# 修改密码
    path('set_password/', views.set_password, name='ser_pwd'),
后端
@login_required()
def set_password(request):
    if request.is_ajax():
        back_dic = {"code": 1000, 'msg': ''}
        if request.method == 'POST':
            old_password = request.POST.get('old_password')
            new_password = request.POST.get('new_password')
            confirm_password = request.POST.get('confirm_password')
            is_right = request.user.check_password(old_password)
            if is_right:
                if new_password == confirm_password:
                    request.user.set_password(new_password)
                    request.user.save()
                    back_dic['msg'] = '修改成功'
                    back_dic['url'] = '/home/'
                else:
                    back_dic['code'] = 1001
                    back_dic['msg'] = '两次密码不一致'
            else:
                back_dic['code'] = 1002
                back_dic['msg'] = '原密码错误'
        return JsonResponse(back_dic)
前端
<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">
                                    <h1 class="text-center">修改密码</h1>
                                    <div class="row">
                                        <div class="col-md-6 col-md-offset-2">
                                            <div class="form-group">
                                                <label for="">用户名</label>
                                                <input type="text" disabled value="{{ request.user.username }}"
                                                       class="form-control">
                                            </div>
                                            <div class="form-group">
                                                <label for="id_old_password">原密码</label>
                                                <input type="password" id='id_old_password' class="form-control">
                                            </div>
                                            <div class="form-group">
                                                <label for="">新密码</label>
                                                <input type="password" id='id_new_password' class="form-control">
                                            </div>
                                            <div class="form-group">
                                                <label for="id_confirm_password">确认密码</label>
                                                <input type="password" id='id_confirm_password' class="form-control">
                                            </div>


                                            <div class="modal-footer">
                                                <button type="button" class="btn btn-default "
                                                        data-dismiss="modal">
                                                    取消&nbsp;
                                                </button>
                                                <button class="btn btn-danger" id="id_edit">确认修改</button>
                                                <span style="color: red" id="password_error" class="pull-left"></span>
                                            </div>
                                            <br>
                                            <br>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
<script>
    $('#id_edit').click(function () {
        $.ajax({
            url: '/set_password/',
            type: 'post',
            data: {
                'old_password': $('#id_old_password').val(),
                'new_password': $('#id_new_password').val(),
                'confirm_password': $('#id_confirm_password').val(),
                'csrfmiddlewaretoken': '{{ csrf_token }}',
            },
            success: function (args) {
                if (args.code == 1000) {
                    location.href = args.url
                } else {
                    //渲染错误信息
                    $('#password_error').text(args.msg)
                }

            }
        })
    })
</script>

九、 admin后台管理

简介

django给你提供了一个可视化的界面用来让你方便的对你的模型表
进行数据的增删改查操作

如果你先想要使用amdin后台管理操作模型表
你需要先注册你的模型表告诉admin你需要操作哪些表

然后去你的应用下的admin.py中注册你的模型表

注册models中的表: 到admin.py中

'''
使用介绍: admin.site.register(需要注册的表名)
'''
from django.contrib import admin
from app01 import models

# Register your models here.
admin.site.register(models.UserInfo)
admin.site.register(models.Blog)
admin.site.register(models.Category)
admin.site.register(models.Article)
admin.site.register(models.Article2Tag)
admin.site.register(models.UpAndDown)
admin.site.register(models.Comment)

修改admin后台管理默认的表名: 在models.py中

class User(models.Model):
    ...
    class Meta:
        verbose_name = '用户表'          # 末尾会加s.  如: 用户表s
        verbose_name_plural = '用户表'   # 末尾不会加s. 如: 用户表

分析admin实现后台管理的url规律

: http://127.0.0.1:8000/admin/app01/userinfo/: http://127.0.0.1:8000/admin/app01/userinfo/add/: http://127.0.0.1:8000/admin/app01/userinfo/1/change/: http://127.0.0.1:8000/admin/app01/userinfo/1/delete/

让后台管理输入时对应字段可以为空

null=True   数据库该字段可以为空
blank=True  admin后台管理该字段可以为空

十、个人站点设计

图片防盗链

图片防盗链

  • 通过referer限制\
  • 在nginx上限制(代码不用限制)
  • nginx不配置,代码中多加一层,中间件
# 简单的防盗
  我可以做到请求来的时候先看看当前请求是从哪个网站过来的
  如果是本网站那么正常访问
  如果是其他网站直接拒绝
    请求头里面有一个专门记录请求来自于哪个网址的参数
    Referer: http://127.0.0.1:8000/xxx/
# 如何避免
  1. 要么修改请求头referer. 修改成与对方Referer一样.
  2. 直接写爬虫把对方网址的所有资源直接下载到我们自己的服务器上

个人站点分析:

# url分析:
# 个人站点分析
    主要是通过用户名来区分每个不同的用户的个人站点
    https://www.cnblogs.com/linhaifeng
    https://www.cnblogs.com/wupeiqi
    https://www.cnblogs.com/liuqingzheng

    因此我们的url因该是:  \w应该是只匹配数字, 字母, 下划线. 为什么这里匹配也会匹配横杠-???
    url(r'^(?P<username>\w+)/$', views.site)

# 侧边栏分析
    主要是通过用户名/分类项/分类主键值来进行对文章的分类
    https://www.cnblogs.com/liuqingzheng/archive/2019/10.html
    https://www.cnblogs.com/liuqingzheng/category/1330586.html
    https://www.cnblogs.com/liuqingzheng/tag/1330250.html

    因此我们的url因该是:
    url(r'^(?P<username>\w+)/tag/(?P<param>\d+)/', views.site)
    url(r'^(?P<username>\w+)/category/(?P<param>\d+)/', views.site)
    url(r'^(?P<username>\w+)/archive/(?P<param>\w+)/', views.site)
    以上三条可以合并成一条:
    url(r'^(?P<username>\w+)/(?P<condition>archive|category|tag)/(?P<param>.*)/', views.site)

# 由于url方法第一个参数是正则表达式,所有当路由特别多的时候,可能会出现被顶替的情况,针对这种情况有两种解决方式
1.修改正则表达式
2.调整url方法的位置

# 个人站点
# 样式: 每个用户都可以有自己的站点样式
    内部给每个人开设了可以自定义css和js的文件接口并且用户自定义之后会将用户的文件保存下来,
    之后在打开用户界面的时候会自动加载用户自己写的css和js从而实现每个用户界面不一样的情况
    <link rel="stylesheet" href="/media/css/{{ blog.site_theme }}/">

# 导航栏
    导航栏需要跟随不同用户的登录用户, 展示不同的用户站点标题. 其它部分不变

# 页面
    采用container布局. 左右2边留白. 采用3 9布局
    侧边栏要实现文章分类, 标签分类, 日期归档
    文章内容展示文章标题, 文章详情等等. 还是可以使用分页器.
        文章分类: 拿到文章名, 文章对应的个数
        文章标签: 拿到标签, 标签下文章对应的个数
        文章日期归档: 拿到文章创建日期, 进行分类统计对应日期下的文章个数

# 视图函数逻辑
# 文章内容部分
    1. 先判断username用户名存不存在
        1-1. 存在返回用户对象
        1-2. 不存在返回error.html
    2. 通过用户对象, 获取对应用户下的个人站点. 使用子查询, 获取到对应用户个人站点下的所有文章queryset对象
    3. 将所有文章queryset对象传入html页面进行渲染

# 侧边栏实现分类
    1. 由于上面是多条路由匹配一个视图函数, 争对匹配侧边栏的路由, 视图函数中我们使用**kwargs来接收
    2. 判断**kwargs中有没有值, 如果有值那么就是需要执行分组
    3. 文章分类和文章标签就通过传过来的主键值进行筛选, 对于日期归档就使用__year和__month进行筛选. 最终过滤出符合条件的结果

# 文章分类查询: 这里的pk是传给html页面进行url分组的参数, 用来匹配对应分类的主键下的文章数
    from django.db.models import Count
    models.Category.objects.filter(blog=blog).annotate(article_num=Count('article')).values_list('name', 'article_num', 'pk')

# 文章标签查询: 这里的pk是传给html页面进行url分组的参数, 用来匹配对应标签的主键下的文章数
    models.Tag.objects.filter(blog=blog).annotate(article_num=Count('article')).values_list('name', 'article_num', 'pk')

# 日期归档查询
    '''
    django官网提供的一个orm语法
    from django.db.models.functions import TruncMonth
         Sales.objects
        .annotate(month=TruncMonth('timestamp'))  # Truncate to month and add to select list
        .values('month')          # Group By month
        .annotate(c=Count('id'))  # Select the count of the grouping
        .values('month', 'c')     # (might be redundant, haven't tested) select month and count
    '''
    from django.db.models.functions import TruncMonth
    models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('update_time')).values('month').annotate(article_num=Count('pk')).values_list('month', 'article_num')

    原理: 先通过annotate(month=TruncMonth('update_time'))截断到月并添加到选择列表, 如下所示
    id		  content 			             update_time				 month
    1			111							 2021-11-11					2021-11
    2			222							 2022-11-12					2022-11
    3			333							 2023-11-13					2023-11
    4			444							 2024-11-14					2024-11
    5			555							 2025-11-15					2025-11

个人站点路由设计

# 个人站点路由(这个路径必须放在最后面,如果放在前面,其他路径都会有问题)
    re_path('^(?P<username>\w+)/$', views.personal_site, name='site'),

个人站点页面设计

base.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>
        {% block title %}

        {% endblock %}
    </title>
    <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="/static/font-awesome-4.7.0/css/font-awesome.min.css">
    <script src="/static/jquery-3.3.1.js"></script>
    <script src="/static/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
    {% block js %}

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

<div class="head" style="margin-bottom: 10px">

    <div style="height: 120px;background-color: #1b6d85">
        <div class="row">
            <div class="col-md-2" style="font-size: 30px;color: white;margin: 20px">
                {% block site_name %}

                {% endblock %}
            </div>
        </div>
        <div class="row">
            <div class="pull-right" style="margin-right: 20px">
                <span><a href="">后台管理</a></span>
                <span>首页</span>
                <span>订阅</span>
            </div>
        </div>
    </div>
</div>
<div class="container-fluid">
    <div class="row">
        {% block left %}

        {% endblock %}
        {% block main %}

        {% endblock %}

    </div>
</div>

</body>
</html>
site.html
{% extends 'base.html' %}

{% block title %}
    {{ user.username }}-博客园
{% endblock %}

{% block site_name %}
    {{ user.blog.site_name }}
{% endblock %}

{% block left %}
    <div class="col-md-2">
        <div class="panel panel-success">
            <div class="panel-heading">
                <h3 class="panel-title">我的标签</h3>
            </div>
            <div class="panel-body">
                {% for tag in tag_list %}
                    <p><a href="">{{ tag.1 }}({{ tag.0 }})</a></p>
                {% endfor %}


            </div>
        </div>
        <div class="panel panel-info">
            <div class="panel-heading">
                <h3 class="panel-title">我的分类</h3>
            </div>
            <div class="panel-body">
                {% for category in category_list %}
                    <p><a href="">{{ category.name }}({{ category.count }})</a></p>
                {% endfor %}
            </div>
        </div>
        <div class="panel panel-info">
            <div class="panel-heading">
                <h3 class="panel-title">随笔档案</h3>
            </div>
            <div class="panel-body">
                {% for month in month_list %}

                    <p><a href="">{{ month.0|date:'Y年m月' }}({{ month.1 }})</a></p>
                {% endfor %}

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

{% block main %}
    <div class="col-md-10">
        <div class="article" style="margin-top: 20px">
            {% for article in article_list %}
                <div>
                    <h4 class="media-heading"><a href="">{{ article.title }}</a></h4>
                    <div class="media">
                        <div class="media-body">
                            {{ article.desc }}

                        </div>
                        <div style="margin-top: 10px" class="pull-right">
                            <span class="article_footer">posted @</span>
                            <span class="article_footer">{{ article.create_time|date:'Y-m-d H:i:s' }}</span>
                            {#                                <span class="glyphicon glyphicon-thumbs-up article_footer">&nbsp;{{ article.up_num }}</span>#}
                            <span class="article_footer"><i
                                    class="fa fa-comment-o fa-lg"></i>&nbsp;{{ article.up_num }}</span>
                            <span class="article_footer"><i
                                    class="fa fa-home fa-lg"></i>&nbsp;{{ article.down_num }}</span>
                            <span class='article_footer'><i
                                    class="fa fa-frown-o fa-lg"></i>&nbsp;{{ article.commit_num }}</span>
                        </div>
                    </div>
                    <hr>
                </div>
            {% endfor %}


        </div>

    </div>
{% endblock %}
左侧过滤功能
# 查询当前站点下某年某月的文章数(分组依据,日期:年月),不需要连表
from django.db.models.functions import TruncMonth, TruncDay, TruncYear, TruncHour
'''
Sales.objects
.annotate(month=TruncMonth('timestamp'))  # Truncate to month and add to select list
.values('month')  # Group By month
.annotate(c=Count('id'))  # Select the count of the grouping
.values('month', 'c')  # (might be redundant, haven't tested) select month and count
'''


id   name         create_time        month
1    xx          2020-09-18:xxx      2020-09
2    xx44        2020-09-18:xxx      2020-09
    
3    xx44        2020-08-18:xxx      2020-08
from django.db.models import Count
def personal_site(request, username):
    user = models.UserInfo.objects.filter(username=username).first()
    if user:
        article_list = user.blog.article_set.all()
        # category_list = user.blog.category_set.all()
        # tag_list = user.blog.tag_set.all()
        # category_list=user.blog.category_set.all()

        # 查询每个分类下的文章数和分类名称
        # category_list=models.Category.objects.values('id').annotate(count=Count('article')).values('count','name')
        # print(category_list)

        # 查询当前这个站点下的分类及分类下的文章数
        category_list = models.Category.objects.filter(blog=user.blog).annotate(count=Count('article')).values('count', 'name')
        # category_list = models.Category.objects.filter(blog=user.blog).annotate(count=Count('article')).values_list('count', 'name')
        print(category_list)

        #查询当前站点下所有的标签及标签下的文章数

        # tag_list=models.Tag.objects.filter(blog=user.blog).annotate(count=Count('article')).values('count', 'name')
        tag_list=models.Tag.objects.filter(blog=user.blog).annotate(count=Count('article')).values_list('count', 'name')
        print(tag_list)

        # 查询当前站点下按月分组的文章数和年份月份
        '''
        Sales.objects
        .annotate(month=TruncMonth('timestamp'))  # Truncate to month and add to select list
        .values('month')  # Group By month
        .annotate(c=Count('id'))  # Select the count of the grouping
        .values('month', 'c')  # (might be redundant, haven't tested) select month and count
        '''
        from django.db.models.functions import TruncMonth,TruncYear  # 按月截断
        month_list=models.Article.objects.filter(blog=user.blog).annotate(month=TruncMonth('create_time')).values('month').annotate(count=Count('id')).values_list('month','count')
        print(month_list)
        return render(request, 'site.html', locals())
    else:
        return render(request, '404.html')

左侧查询和个人主页路由整合

路由整合
re_path('^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<params>.*).html$', views.personal_site),
视图函数
def personal_site(request, username, **kwargs):
    user = models.UserInfo.objects.filter(username=username).first()
    if user:

        article_list = user.blog.article_set.all()
        if kwargs:
            condition = kwargs.get('condition')
            params = kwargs.get('params')
            if condition == 'category':
                article_list = article_list.filter(category_id=params)
            elif condition == 'tag':
                article_list = article_list.filter(tag__id=params) #跨表了
            elif condition == 'archive':
                param_year, param_month = params.split('/')
                article_list = article_list.filter(create_time__year=param_year, create_time__month=param_month)



        return render(request, 'site.html', locals())
    else:
        return render(request, '404.html')

十一、 文章详情页

分析

# url设计
    参考: https://www.cnblogs.com/wupeiqi/p/4766801.html
    我们的那因该是:
    url(r'^(?P<username>\w+)/article/(?P<article_id>), views.article_detail)

# 视图函数
    1. 我们因该先验证url是否会顶替的情况
    2. 效验username和article_id是否存在的情况
    3. 继承以后, blog对象需要传, 文章详情通过传过来的用户名筛选出对应用户的文章, 不然别的用户也能通过文章主键值获取到别人的文章详情
    4. 继承以后侧边栏的渲染需要传输数据才能渲染 并且该侧边栏在很多页面都需要使用, 这个时候如果还在article_detail中书写, 那么明显冗余了.
        这个时候我们想到, 有一个页面需要传数据才能渲染, 我们就用到inclusion_tag了.
        将侧边栏制作成inclusion_tag步骤:
            1. 应用下创建templatetags文件夹
            2. 文件夹下定义任意.py文件.: left_menu.py
            3. 文件夹书写如下固定格式代码:
                from django import template
                register = template.Library()
            4. 可以书写自定义的过滤器, 标签, inclusion_tag


# 模板层HTML页面
    1. 文章详情页和个人站点基本一致 所以用模版继承.
        文章详情页和个人站点页在 9份的页面布局区域不一样, 其它都一样
    2. 点击个人站点页展示的文章详情的a标签要跳转到对应的文章详情.  主页也是同等, 不过主页没有username因此需要使用跨表查询

左侧标签,分类,归档写成inclusion_tag

left.py
from django import template
register = template.Library()
from blog import models
from django.db.models.functions import TruncMonth
from django.db.models import Count

@register.inclusion_tag('left.html')
def left_inclusion_tag(username):
    user=models.UserInfo.objects.filter(username=username).first()
    category_list = models.Category.objects.filter(blog=user.blog).annotate(count=Count('article')).values('count',
                                                                                                           'name', 'id')
    tag_list = models.Tag.objects.filter(blog=user.blog).annotate(count=Count('article')).values_list('count',
                                                                                                      'name', 'id')

    month_list = models.Article.objects.filter(blog=user.blog).annotate(month=TruncMonth('create_time')).values(
        'month').annotate(count=Count('id')).values_list('month', 'count')

    return {'username':username,'category_list':category_list,'tag_list':tag_list,'month_list':month_list}
left.html
<div>
    <div class="panel panel-success">
        <div class="panel-heading">
            <h3 class="panel-title">我的标签</h3>
        </div>
        <div class="panel-body">
            {% for tag in tag_list %}
                <p><a href="/{{ username }}/tag/{{ tag.2 }}.html">{{ tag.1 }}({{ tag.0 }})</a></p>
            {% endfor %}


        </div>
    </div>
    <div class="panel panel-info">
        <div class="panel-heading">
            <h3 class="panel-title">我的分类</h3>
        </div>
        <div class="panel-body">
            {% for category in category_list %}
                <p>
                    <a href="/{{ username }}/category/{{ category.id }}.html">{{ category.name }}({{ category.count }})</a>
                </p>
            {% endfor %}
        </div>
    </div>
    <div class="panel panel-info">
        <div class="panel-heading">
            <h3 class="panel-title">随笔档案</h3>
        </div>
        <div class="panel-body">
            {% for month in month_list %}

                <p>
                    <a href="/{{ username }}/archive/{{ month.0|date:'Y/m' }}.html">{{ month.0|date:'Y年m月' }}({{ month.1 }})</a>
                </p>
            {% endfor %}

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

使用

{% load left %}
{% left_inclusion_tag username %}

十二、文章点赞点踩

分析

# 文章内容的拷贝展示
描述: 浏览器上你看到的花里胡哨的页面,内部都是HTML(前端)代码
疑问: 那现在我们的文章内容应该写什么???	>>> html代码
如何拷贝文章: 进入浏览器 copy outerhtml

# 点赞点踩拷贝展示
需要拷贝的内容有, html, css
对于css需要按照层级一层一层的拷贝.
注意: 对于图片的拷贝, 涉及到防盗链, 我们有2种方式解决.
    第一种方式: 修改请求头referer.  下载图片到本地
    第二种方式: 写爬虫把对方网址的所有资源直接下载到我们自己的服务器上
提示: 拷贝的html中含有它们的点击事件, 我们换成我们自己的

# 思考: 前端如何区分用户是点了赞还是点了踩
1.给标签各自绑定一个事件
    两个标签对应的代码其实基本一样,仅仅是是否点赞点踩这一个参数不一样而已
2.二合一
    给两个标签绑定一个事件
    //   给所有的action类绑定事件
    $('.action').click(function () {
        alert($(this).hasClass('diggit'))
    })

# 提示: 由于点赞点踩内部有一定的业务逻辑,所以后端单独开设视图函数处理

# 视图函数逻辑
# 提示: 先写正确的的业务逻辑
1. 判断用户是否登录
    request.user.is_authenticated()
2. 判断当前文章是否是当前用户自己写的(自己不能点自己的文章)
    根据文章id, 获取文章对象, 更具文章对象跨表查询用户对象  与 点赞用户对象进行对比
3. 当前用户是否已经给当前文章点过了
    根据点踩点赞表判断文章id 用户id 是否一致, 如果是那么就是点过了.
4. 操作数据库. 操作点赞点踩表时, 记得同步操作文章的点赞点踩对普通字段+1

# 前端业务逻辑
# 正确的逻辑有
    先使用模板语法展示点赞点踩数
    在用户点赞或者点踩以后先获取, 原来的文本内容(注意: 是字符串需要parseInt()), 再进行加一既可

# 错误的逻辑有
    用户没有登录时: 请先<a href="/app01/login">登录</a>
    用户点自己的时: 不能推荐自己的内容
    用户已经点过时: 对点过赞 还是点过踩加以区分
        点过赞: 您已经支持过
        点过踩: 您已经反对过
    注意: 以上涉及到标签文本, 所以我们使用html

# 展示效果:
    用户点击时先展示: 提交中...
    点击完毕以后展示: 对应的提示文本

# 拓展: 展示取消
# 前端页面
    前提: 如果当前用户已经登录并且该用户已经点过赞时显示用户可以取消点赞或者点踩
    1. 判断用户是否登录, 并且已经对这篇文章点过赞或者踩:
        判断已经对这篇文章点赞:  判断当前登录用户的用户user_id  和 当前文章的article_id 是否存在.
        存在则表示当前文章已经被当前登录的用户点过了, 那么就展示对应的文本
            判断点赞时展示: <span style="color:#808080;">您已推荐过,<a class="a_cancel cancel_up">取消</a></span>
            判断点踩时展示: <span style="color:#808080;">您已反对过,<a class="a_cancel">取消</a></span>
        不存在则表示当前文章没有被当前登录的用户点过, 那么着展示文本就不展示
    2. 为点过, 点赞a标签和点踩a标签都指定一个共同的类属性a_cancel. 然后绑定点击事件
    3. 然后在使用this 合理的判断 cancel_up 是否存在
        true 则表示当前的操作是想取消点赞的
        false则表示当前的操作是想取消点踩的
    4. 可以发送ajax请求了, 但是涉及到url了, 这个时候我们到后端在开设一个取消点赞 点踩的逻辑
    5. 发送数据要指定的有: article_id, 判断取消点赞 还是 取消点踩的 cancel_up.  指定post发送的话, 需要指定csrf.

# 后端逻辑
    1. 先判断是否是ajax请求
    2. 再判断是否是post请求
    3. 直接获取前端传过来的 article_id 和 cancel_up. 注意: cancel_up是字符串类型的json格式数据, 需要反序列化成python格式数据
    4. 文章article_id有了, 当前请求的用户可以通过request.user获取, 现在可以直接进行数据的修改, 和删除了.
        这里涉及到数据的修改, 和删除我们最好使用事务处理.
    5. 先判断用户是否是取消点踩 还是 点赞的逻辑
        取消点踩的就修改文章表中点踩的down_num减一
        取消点赞的就修改文章表中点踩的up_num减一
    6. 无论是点赞点踩都删除UpAndDown表中对应的数据

文章点赞点踩样式

   <div id="div_digg">
            <div class="diggit action">
                <span class="diggnum" id="digg_count">{{ article.up_num }}</span>
            </div>
            <div class="buryit action">
                <span class="burynum" id="bury_count">{{ article.down_num }}</span>
            </div>
            <div class="clear"></div>
            <div class="diggword" id="digg_tips">

            </div>
        </div>

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

文章点赞点踩前后端

后端
def upanddown(request):
    if request.is_ajax():
        response = {'status': 100, 'msg': None}
        if request.user.is_authenticated:
            article_id = request.POST.get('article_id')
            is_up = json.loads(request.POST.get('is_up'))
            res = models.UpAndDown.objects.filter(article_id=article_id, user=request.user)
            if res:
                # 点过了
                response['status'] = 102
                response['msg'] = '你已经点过了'
                return JsonResponse(response)
            # 事务操作
            with transaction.atomic():
                if is_up:
                    models.Article.objects.filter(id=article_id).update(up_num=F('up_num') + 1)
                    response['msg'] = '点赞成功'
                else:
                    models.Article.objects.filter(id=article_id).update(down_num=F('down_num') + 1)
                    response['msg'] = '点踩成功'

                models.UpAndDown.objects.create(user=request.user, article_id=article_id, is_up=is_up)
            return JsonResponse(response)
        else:
            response['status'] = '101'
            response['msg'] = '请去<a href="/login/">登录</a>'
            return JsonResponse(response)
前端
   $('.action').click(function () {
        var is_up=$(this).hasClass('diggit')
        var span= $(this).children('span')
        $.ajax({
            url:'/upanddown/',
            method:'post',
            //谁(当前登录用户)对那篇文章点赞或点踩
            data:{article_id:'{{ article.id }}',is_up:is_up,'csrfmiddlewaretoken':'{{ csrf_token }}'},
            success:function (data) {
                console.log(data)

                $('#digg_tips').html(data.msg)
                if (data.status=='100'){
                    //let num=Number(span.html())+1
                    span.html(Number(span.html())+1)
                }

            }
        })

    })

十三、文章评论

评论分析

# 处理规则: 先处理根评论, 再处理子评论
# 前端
# 处理根评论逻辑
1. 对评论的提交按钮绑定点击事件
    前提: 前端先通过模板语法的if判断, 先判断request.user.is_authenticated用户是否登录
    登录以后, 展示评论功能
    没有登录不展示, 展示提示登录的信息
2. 点击评论提交按钮, 触发点击事件, 使用js的val()方法获取文本域评论框中的内容.
3. 发送ajax请求,
    发送的请求中需要有的参数有: 评论的文章的主键值, 评论的内容, 因为基于post请求, 需要指定csrfmiddlewaretoken
    提示: 不需要指定评论的用户的主键值, 因为后端直接可以通过request.user.pk获取
4. 在后端返回正确提示时, 前端又需要做的2件事:
    第一件事: 评论框清空.
        解决: 通过标签查找到评论框, 使用jq提供的.val('')进行清空
    第二件事: 对评论楼当前的评论进行DOM临时渲染
        解决: 之前我们保存的用户评论的内容content
        我们再通过js的模板语法构造一个临时渲染的标签组, 再通过jq提供的.append()将临时渲染的内容追加到评论楼的末尾

# 处理子评论逻辑
5. 关键之处在于用户点击恢复按钮, 触发点击事件, 通过对所有的回复按钮绑定点击事件, 在用户点击以后, 会发生如下的几件事:
    关标聚焦到评论框中.
        解决: 通过jq提供的.focus()方法
    评论框的格式是: @回复的用户名\n换行到下一行
        解决: 通过当前渲染评论的comment_obj跨表查询获取到对应的用户名,
        不过还需要知道该评论所评论的评论的主键值. 因此: 我们可以通过对使用for循环渲染的评论区域时, 为每一个评论的内容自定义属性
        我们自定义2, 一个属性中值存储评论的用户, 一个属性中值存储评论的主键值
6. 通过对步骤五的2个变量提升到全局, 再通过用户点击评论按钮触发点击事件, 将评论的评论主键值传递过去作为parent_id.
    开始定义的全局定义即可, 不赋值.
    如果用户是子评论, 后端就可以存按照子评论存
    如果不是子评论, 那么后端默认就是存空, 而存空就是根评论
7. 需要注意的是, 往后端存储的数据, 也有了@username\n这样的内容, 我们要先进行indexOf和slice处理
8. 在后端返回成功结果的最后, 我们还需要对评论的内容进行处理,


# 后端
# 处理根评论逻辑
1. 评论功能涉及到许多功能, 需要开辟新的url以及对应的视图函数处理.
2. 基本判断: 先判断是否是ajax请求, 在判断请求方式是否是post, 再判断是否是登录的用户
3. 评论判断:
    先通过POST.get方法获取ajax中的data参数的数据. 包含: 文章的主键值, 评论的内容
    再通过ORM语句:
        先通过文章的主键值找到本次评论的文章, 对该表的评论comment_num普通字段+1
        再对评论表进行新增数据, 新增的数据有: 对应的文章主键值, 评论的内容, 评论的用户
4. 在用户每次发起get获取文章详情页面以后, 都需要对文章的评论楼区域进行渲染,
    因此我们要到文章详情页面对应的视图函数把所有的文章读取出来返回到html页面进行渲染.
# 处理子评论逻辑

评论的render显示

后端
def article_detail(request, username, id):
    article = models.Article.objects.filter(id=id).first()

    comment_list=article.commit_set.all()
    return render(request, 'article.html', {'article': article, 'username': username,'comment_list':comment_list})
前端
<div>
            <h3>评论列表</h3>
            <div>
                <ul class="list-group">
                    {% for comment in comment_list %}

                        <li class="list-group-item">
                            <div style="margin-bottom: 5px">
                                <span>#{{ forloop.counter }}楼</span>
                                <span>{{ comment.create_time|date:'Y-m-d H:i:s' }}</span>
                                <span><a href="/{{ comment.user.username }}">{{ comment.user.username }}</a></span>
                                <span class="pull-right"><a href="">回复</a></span>
                            </div>
                            <div>

                                {% if comment.commit_id %}
                                    <div class="well well-sm">
                                        <p>@{{ comment.commit_id.user.username }}</p>
                                        <p>{{ comment.commit_id.content }}</p>
                                    </div>
                                {% endif %}

                                {{ comment.content }}

                            </div>

                        </li>
                    {% endfor %}


                </ul>
            </div>

        </div>
根评论的ajax提交和显示
$('#id_btn_submit').click(function () {
            $.ajax({
                url: '/comment/',
                method: 'post',
                data: {article_id:'{{ article.id }}',content:$('#id_textarea').val(), 'csrfmiddlewaretoken': '{{ csrf_token }}'},
                success:function (data) {
                    console.log(data)
                    if(data.status==100){
                        let username=data.username
                        let res_content=data.res_content
                        let res=`
                            <li class="list-group-item">
                                <div class="glyphicon glyphicon-headphones"><span>&nbsp;${username}</span></div>
                                <div>
                                 ${res_content}
                                </div>
                            </li>`
                        //把这个字符串追加到
                        $('.list-group').append(res)
                        //清空输入框
                        $('#id_textarea').val('')
                    }

                }
            })

        })
评论后端
def comment(request):
    if request.is_ajax():
        article_id=request.POST.get('article_id')
        content=request.POST.get('content')
        res=models.Commit.objects.create(user=request.user,article_id=article_id,content=content)
        return JsonResponse({'status':100,'msg':'评论成功','username':request.user.username,'res_content':res.content})

子评论ajax提交和显示

后端
def comment(request):
    if request.is_ajax():
        respone = {'status': 100, 'msg': '评论成功'}
        article_id = request.POST.get('article_id')
        content = request.POST.get('content')
        parent_id = request.POST.get('parent_id')

        res = models.Commit.objects.create(user=request.user, article_id=article_id, content=content,
                                           commit_id_id=parent_id)
        respone['username'] = request.user.username,
        respone['res_content'] = res.content
        if parent_id:
            respone['parent_name'] = res.commit_id.user.username
            respone['parent_content'] = res.commit_id.content
        return JsonResponse(respone)
前端
var parent_id=''       
$('#id_btn_submit').click(function () {
            let content=$('#id_textarea').val()
            if(parent_id){
                //子评论
                //找content这个文本中第一个\n的位置
                let count=content.indexOf('\n')+1
                //从这个位置往后截断
                content=content.slice(count)
                console.log(content)
            }
            $.ajax({
                url: '/comment/',
                method: 'post',
                data: {
                    article_id: '{{ article.id }}',
                    content:content ,
                    'csrfmiddlewaretoken': '{{ csrf_token }}',
                    'parent_id':parent_id

                },
                success: function (data) {
                    console.log(data)
                    if (data.status == 100) {
                        let username = data.username
                        let res_content = data.res_content
                        let res=''
                        if(parent_id){
                            let parent_name=data.parent_name
                            let parent_content=data.parent_content
                            res = `
                              <li class="list-group-item">
                                <div class="glyphicon glyphicon-headphones"><span>&nbsp;${username}</span></div>
                                <div>
                                   <div class="well">
                                       @${parent_name}--${parent_content}
                                    </div>
                                 ${res_content}
                                </div>
                            </li>\`
                            `
                        }
                        else {
                            res = `
                            <li class="list-group-item">
                                <div class="glyphicon glyphicon-headphones"><span>&nbsp;${username}</span></div>
                                <div>
                                 ${res_content}
                                </div>
                            </li>`
                        }

                        //把这个字符串追加到
                        $('.list-group').append(res)
                        //清空输入框
                        $('#id_textarea').val('')
                        //把parent_id置空
                        parent_id=''
                    }

                }
            })

        })

$('.reply_span').click(function () {
            let username=$(this).attr('username')
            $('#id_textarea').val('@'+username+'\n').focus()
            parent_id=$(this).attr('parent_id')
})

十四、后台管理

分析:

提示: 当一个文件夹下文件比较多的时候 你还可以继续创建文件夹分类处理
    templates文件夹
        backend文件夹
        应用1文件夹
        应用2文件夹

1. url另起炉灶, url最好按照匹配范围, 放到上面. 可以先测试, 会不会被上面的覆盖.
2. 后台管理布局
    布局 2 10 布局
    含有导航栏, 侧边栏, 面包屑的内容部分展示所有的文章.
    注意: 用户在选择面包屑的每个功能的时候, 页面的导航栏, 面包屑的导航, 以及侧边栏不变, 只是内容部分在变化.
        因此我们用模板的继承, 每个不同的面包屑是一个单独的功能页面.

后台管理首页文章显示

前端
{% extends 'backend/backend_base.html' %}


{% block article %}

    <table class="table table-striped">
      <thead>
        <tr>
          <th>文章id</th>
          <th>文章标题</th>
          <th>文章作者</th>
          <th>点赞数</th>
          <th>点踩数</th>
          <th>操作</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
{% for article in  article_list%}
         <tr>
          <th scope="row">{{ article.id }}</th>
          <td><a href="/{{ article.blog.userinfo.username }}/articles/{{ article.id }}.html">{{ article.title }}</a></td>
          <td>{{ article.blog.userinfo.username }}</td>
          <td>{{ article.up_num }}</td>
          <td>{{ article.down_num }}</td>
          <td><a href="">删除</a></td>
          <td><a href="">编辑</a></td>
        </tr>

{% endfor %}


      </tbody>
    </table>
{% endblock %}
后端
@login_required(login_url='/login/')
def backend(request):
    article_list = models.Article.objects.filter(blog=request.user.blog)
    return render(request, 'backend/backend_index.html', locals())

十五、新增文章(富文本编辑器,xss攻击)

参考网址: http://kindeditor.net/doc.php

分析

# 添加文章需要注意的问题
    1. 文章的简介不能直接切去应该先想办法获取到当前页面的文本内容之后截取150个文本字符
    2. XSS攻击.
        针对支持用户直接编写html代码的网址
        以及针对用户直接书写的script标签 我们需要处理这种情况, 以下是2种处理的方式:
          1. 注释标签内部的内容
            <p>
                <script type="text/javascript">// <![CDATA[alert('xx');// ]]></script>
            </p>
          2. 直接将script删除

# 解决方式: BeautifulSoup模块
    # 作用: 获取html页面的文本内容, 删除对应的标签解决xss跨站脚本攻击
    # 下载: pip3 install bs4
    # 使用:
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(connect, 'html.parser')
            第一个参数: 网页内容
            第二个参数: 解析器. 分别有: lxml, lxml-xml, html.parser, html5lib
        tags = soup.find_all()
            返回值: 是一个列表, 列表中的每个元素都是标签和文本对象
        for tag in tags:           # 1. 处理xss跨站脚本攻击. 处理方式: 直接将script删除
            if tag.name == 'script':
                tag.decompose()
        desc = super.text[0:150]   # 2. 获取html页面的文本内容
        str(soup)                  # 3. 将处理好的结果又转成成网页内容的格式
后端
@login_required(login_url='/login/')
def add_article(request):
    if request.method == 'GET':

        # ss='&lt;a href="http://www.baicu.com">点我看美女</a>'
        category_list = models.Category.objects.filter(blog=request.user.blog)
        tag_list = models.Tag.objects.filter(blog=request.user.blog)
        return render(request, 'backend/add_article.html', locals())
    else:

        title = request.POST.get('title')
        content = request.POST.get('content')

        # 第一个参数是要解析的html文档内容(str)
        # 第二个参数是使用的解析器(html.parser和lxml)
        soup = BeautifulSoup(content, 'html.parser')
        # 去掉所有标签后的文本内容
        desc = soup.text[:250]
        # 找出文档中所有script标签()
        script_list = soup.find_all('script')
        for script in script_list:
            print('删了一个')
            script.decompose()  # 把当前标签对象,从文档中删除
        category_id = request.POST.get('category')
        tag_ids = request.POST.getlist('tag')
        article = models.Article.objects.create(title=title, desc=desc, content=str(soup), blog=request.user.blog,
                                                category_id=category_id)
        # 手写的第三张表,这个已经没有了
        # article.tag.add()
        # 手动操作
        # for tag_id in tag_ids:
        #     models.TagToArticle(article_id=article.id,tag_id=tag_id)

        # 批量插入
        tag_list = []
        for tag_id in tag_ids:
            tag_list.append(models.TagToArticle(tag_id=tag_id, article_id=article.id))
        models.TagToArticle.objects.bulk_create(tag_list)

        return redirect('/backend/')

十六、富文本编辑器上传图片

前端

       KindEditor.ready(function (K) {
            window.editor = K.create('#id_textarea', {
                width: '100%',
                height: '600px',
                resizeType: 1,
                uploadJson:'/upload_img/',
                extraFileUploadParams : {
                        csrfmiddlewaretoken:'{{ csrf_token }}'
                }

            });
        });

后端

# @csrf_exempt
def upload_img(request):
    print(request.FILES)
    try:
        myfile = request.FILES.get('imgFile')
        path = os.path.join(settings.MEDIA_ROOT, 'img')
        with open('%s/%s' % (path, myfile.name), 'wb') as f:
            for line in myfile:
                f.write(line)
        return JsonResponse({"error": 0, "url": '/media/img/'+myfile.name})
    except Exception as e:
        return JsonResponse({"error": 1,"message": str(e)})

十七、django发送邮件

https://www.cnblogs.com/liuqingzheng/articles/10072695.html
# 配置文件
EMAIL_HOST = 'smtp.qq.com'  # 如果是 163 改成 smtp.163.com
EMAIL_PORT = 465
EMAIL_HOST_USER = '306334678@qq.com'  # 帐号
EMAIL_HOST_PASSWORD = ''  # 密码
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
#这样收到的邮件,收件人处就会这样显示
# DEFAULT_FROM_EMAIL = '暗示法司法'
EMAIL_USE_SSL = True   #使用ssl
#EMAIL_USE_TLS = False # 使用tls

# 视图函数
from django.core.mail import send_mail
send_mail("邮件标题" ,'邮件内容',settings.EMAIL_HOST_USER,["2277194535@qq.com"])

十八、修改用户头像

分析

# 争对FileField字段存储的的路径. 有以下2种的修改方式:
# 方式一: 争对所有数据的update. 缺点: 无法补全之前指定的upload_to参数的前缀
    models.UserInfo.objects.filter(pk=request.user.pk).update(avatar=avatar_obj)
# 方式二: 争对单个数据的update. 优点: 可以补全之前指定的upload_to参数的前缀
    user_obj = request.user
    user_obj.avatar = avatar_obj
    user_obj.save()

前端

{% extends 'base.html' %}


{% block content %}
    <h3 class="text-center">修改头像</h3>
    {% load static %}
    <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <p>
            原头像:
            <img src="/media/{{ request.user.avatar }}" alt="">
        </p>
        <p>
            新头像:
            <label for="myfile">头像
                <img src="{% static 'img/default.png' %}" id="myimg" alt="" width="100" style="margin-left:5px">
            </label>
            <input type="file" id="myfile" name="avatar" style="display: none">
        </p>
        <input type="submit" class="btn btn-info">
    </form>
{% endblock %}

{% block js %}
    <script>
        $('#myfile').change(function () {
            {#    文件阅读器#}
            {#    1.生成一个文件阅读器对象    #}
            let myFileReaderObj = new FileReader()
            {#    2.获取用户上传的头像文件    #}
            let fileObj = $(this)[0].files[0]
            {#    3.将文件对象交给阅读器对象读取#}
            myFileReaderObj.readAsDataURL(fileObj)
            {#异步操作 IO操作#}
            {#    4.利用文件阅读器将文件展示到前端页面 修改src属性#}
            {#      等待XXX加载完毕在执行#}
            myFileReaderObj.onload = function () {
                $('#myimg').attr('src', myFileReaderObj.result)
            }
        })
    </script>
{% endblock %}

后端

def set_avatar(request):
    if request.method == "POST":
        file_obj = request.FILES.get('avatar')
        models.UserInfo.objects.filter(id=request.user.id).update(avatar=file_obj)  # 不会再自动加avatar前缀
        # 1.自己手动加
        # 2.换一种更新方式
        user_obj = request.user
        user_obj.avatar = file_obj
        user_obj.save()
        return redirect('/home/')
    blog = request.user.blog
    username = request.user.username
    return render(request, 'set_avatar.html', locals())

补充:

扩展一:
1 Django:django的orm,rbac,admin
	-admin:bug级的存在,几乎不用写代码,就可以撸出一个后台管理系统
    -xadmin:django3.0以后,作者弃坑,不管了  1.x版本  2.x版本:用起来还是可以的
    -3.x版本:simpleui
2 flask
----------同步框架-------
----------django 3.x以后支持异步------
---------一旦用了异步,所有的都要用异步-----
	-python线程中只要有io操作,就会让出gil锁,这条线程下次要执行必须再拿到gil
    -协程:单线程下实现并发,程序员自己控制切换


-----往下的是异步框架------
3 tornado:2.x
4 FastAPI:操作数据库
5 Sanic:python 3.5以上,不支持windows

6 到目前为止没有一个好用的异步orm框架
7 异步操作模块:mysql,redis,mongodb


mac,乌班图(台式机),使用window远程连接linux开发,纯windows
jquery 的each的用法
$.each([ 52, 97 ], function( index, value ) {
  alert( index + ": " + value );
});

点我下载代码

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贾维斯Echo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值