15 BBS

一. 项目开发流程

# 1.需求分析
    架构师+产品经理+开发者组长
    在跟客户谈需求之前,会大致先了解客户的需求,然后自己先设计一套比较好写方案
    在跟客户沟通交流中引导客户往我们之前想好的方案上面靠
    形成一个初步的方案

# 2.项目设计
    架构师干的活
    编程语言选择
    框架选择
    数据库选择
        主库:MySQL,postgreSQL,...
        缓存数据库:redis、mongodb、memcache...
    功能划分
        将整个项目划分成几个功能模块
    找组长开会
        给每个组分发任务
    项目报价
        技术这块需要多少人,多少天(一个程序员一天1500~2000计算(大致))
      产品经理公司层面 再加点钱
        公司财务签字确认
        公司老板签字确认
      产品经理去跟客户沟通

      后续需要加功能 继续加钱

# 3.分组开发
    组长找组员开会,安排各自功能模块
  我们其实就是在架构师设计好的框架里面填写代码而已(码畜)

  我们在写代码的时候 写完需要自己先测试是否有bug
  如果是一些显而易见的bug,你没有避免而是直接交给了测试部门测出来
  那你可能就需要被扣绩效了(一定要跟测试小姐姐搞好关系)
    薪资组成	15K(合理合规合法的避税)
      底薪	10K
      绩效	3K
      岗位津贴 1K
      生活补贴 1K

# 4.测试
    测试部门测试你的代码
    压力测试
    ...

# 5.交付上线
    1.交给对方的运维人员
  2.直接上线到我们的服务器上 收取维护费用
  3.其他...

二. BBS表设计

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

# 数据库选择: 由于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是根评论. 根评论为空作为标记

三. 数据库表创建及同步

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


# Create your models here.
class MyCharField(models.Field):
    def __init__(self, max_length, *args, **kwargs):
        self.max_length = max_length
        super(MyCharField, self).__init__(max_length=max_length, *args, **kwargs)

    def db_type(self, connection):
        return f'Char({self.max_length})'


class UserInfo(AbstractUser):
    gender_choices = (
        (0, '女'),
        (1, '男'),
        (2, '保密'),
    )
    gender = models.IntegerField(default=2, choices=gender_choices, verbose_name='用户性别')
    phone = models.BigIntegerField(null=True, verbose_name='用户手机号码', blank=True)
    # 给avatar字段传文件对象 该文件会自动存储到avatar文件下 然后avatar字段只保存文件路径avatar/default.png
    avatar = models.FileField(upload_to='avatar/', default='avatar/default.png', verbose_name='用户头像')
    addr = models.CharField(null=True, max_length=128, verbose_name='用户家庭住址', blank=True)
    create_time = models.DateField(auto_now_add=True, verbose_name='用户创建时间')

    # 一对一个人站点外键
    blog = models.OneToOneField(to='Blog', null=True)

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

    def __str__(self):
        return self.username


class Blog(models.Model):
    site_name = models.CharField(max_length=32, verbose_name='站点名称')
    site_title = models.CharField(max_length=64, verbose_name='站点标题')
    site_theme = models.CharField(max_length=64, verbose_name='站点样式')  # 存css/js的文件路径

    class Meta:
        verbose_name_plural = '个人站点表'

    def __str__(self):
        return self.site_name


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

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

    class Meta:
        verbose_name_plural = '文章分类表'

    def __str__(self):
        return self.name


class Tag(models.Model):
    name = models.CharField(max_length=32, verbose_name='文章标签')

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

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

    def __str__(self):
        return self.name


class Article(models.Model):
    title = models.CharField(max_length=64, verbose_name='文章标题')
    desc = models.CharField(max_length=255, verbose_name='文章描述')
    # 文章内容有很多 一般情况下都是使用TextField
    content = models.TextField(verbose_name='文章内容')
    update_time = models.DateField(auto_now=True, verbose_name='文章更新时间')

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

    # 一对多个人站点
    blog = models.ForeignKey(to='Blog', null=True)
    # 一对多文章分类
    category = models.ForeignKey(to='Category', null=True)
    # 多对多文章标签
    tags = models.ManyToManyField(to='Tag', through='Article2Tag', through_fields=('article', 'tag'))

    class Meta:
        verbose_name_plural = '文章表'

    def __str__(self):
        return self.title


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

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


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

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


class Comment(models.Model):
    user = models.ForeignKey(to='UserInfo')
    article = models.ForeignKey(to='Article')
    content = models.CharField(max_length=255, verbose_name='用户评论内容')
    content_time = models.DateField(auto_now_add=True, verbose_name='用户评论时间')

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

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

四. 注册功能

1. forms组件代码解耦合

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

2. 利用文件阅读器将选中的文件图片展示到前端页面

# HTML部分
<form action="">
    <div class="form-group">
        <label for="id_file">
            用户头像
            {% load static %}
            <img src="{% static 'img/default.png' %}" alt="" title="" width="40" id="id_img">
        </label>
        <input type="file" id="id_file" name="file" style="display: none">
    </div>
</form>


# JS部分
$('#id_file').change(function () {
    // 1. new一个文件阅读器对象
    let fileReaderObj = new FileReader();
    // 2. 获取用户上传的头像文件
    let fileObj = $('#id_file')[0].files[0];
    // 3. 将文件对象交给阅读器对象读取 (注意: readAsDataURL是一个异步操作)
    fileReaderObj.readAsDataURL(fileObj);
    // 4. 利用文件阅读器将文件展示到前端页面. (本质就是修改img的src属性)
    fileReaderObj.onload = function () {
        $('#id_img').attr('src', fileReaderObj.result);
    };
})

3. 在使用forms组件的前提下, 不使用forms表单提交数据, 而是使用ajax发送数据

/*
需要注意的问题:
    1. 使用forms组件就不方便使用标签查找获取到对于的name和value.
    2. 不使用form表单提交数据, 我们不适用submit按钮, 而是使用普通按钮
    3. 本次提交数据中含有文件对象, 因此我们ajax要使用FormData对象发送数据

解决问题的思路: 使用jq标签查找, 找到对应的form表单, 再使用.serializeArray()方法获取所有form表单中的name和value
    数据的表现形式是: [{name: '', value: ''}, {}]
    提示: 该方法无法获取文件对象对应的name,value
*/
$('#id_commit').click(function () {
    let formDataObj = new FormData();
    let serializeArrayObj = $('#id_form').serializeArray();
    let fileObj = $("#id_file")[0].files[0];

    // 添加普通的键值对
    $.each(serializeArrayObj, function (index, obj) {
        formDataObj.append(obj.name, obj.value);
    });
    // 添加文件数据
    formDataObj.append('avatar', fileObj);

    $.ajax({
        url: '',
        type: 'post',
        data: formDataObj,
        contentType: false,
        processData: false,
        success: function (args) {
            if (args.code === 1000) {         // 1. 正确的结果就通过location.href = args.url进行跳转到登录页面
                window.location.href = args.url;
            } else if (args.code === 2000) {  // 2. 错误的结果如果返回的是forms.errors. 它数据的展现格式是{'username': [错误信息1, 可能有错误信息2], ...}
                // 2-1. 错误的结果就去渲染input后面的span把错误内容使用text方法放入span中展示
                // 2-2. 同时可以对相应的的input框设置效验状态如: has-error (注意: 不是设置在input上而是这样<div class="form-group has-success">)
                $.each(args.msg, function (index, obj) {
                    $('#id_' + index).next().text(obj[0]).parent().addClass('has-error');
                });
            }
        }
    });
});


// 可以对input框绑定focus事件, 在用户光标获取input框时动态的删除对应的 has-error样式 以及 错误的span提示文本
$("input").focus(function () {
    $(this).next().text('').parent().removeClass('has-error');
})

五. 登录功能

1. 验证码

1-1. 验证码HTML代码书写(关键: src属性)

# 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
<div class="form-group">
    <label for="id_code">验证码</label>
    <div class="row">
        <div class="col-md-6">
            <input type="text" id="id_code" name="code" class="form-control">
        </div>
        <div class="col-md-6">
            <img src="{% url 'app01_get_code' %}" alt="" style="height: 34px; width: 100%" id="id_img">
        </div>
    </div>
</div>

1-2. 视图函数使用第三方图片相关模块 和 内存管理模块实现随机的图片验证码

ttf字体网址: 点我点我

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

import random
from PIL import Image, ImageDraw, ImageFont

"""
Image:生成图片
ImageDraw:能够在图片上乱涂乱画
ImageFont:控制字体样式
"""
from io import BytesIO, StringIO

"""
内存管理器模块
BytesIO:临时帮你存储数据 返回的时候数据是二进制
StringIO:临时帮你存储数据 返回的时候数据是字符串
"""
def get_random():
    """制作随机的三基色"""
    return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)


def get_code(request):
    """
    创建验证码图片步骤流程:
        1. 下载对应的第三方图片相关模块
            pip install pillow
        2. 
2. 
            from PIL import Image, ImageDraw, ImageFont
            image_obj = Image.new('RGB', (width, height), 随机三基色)
        3. 再使用ImageDraw生成一个画笔对象
            draw_obj = ImageDraw.Draw(image_obj)
        4. 再使用ImageFont生成一个字体对象. 并指定字体的格式 和 字体大小
            font_obj = ImageFont.truetype(存放ttf字体格式的路径, size)
        5. 书写验证码逻辑. 注意: 每生成一个随机验证码就用画笔画一个字体
            code = ''
            for i in range(1, 6):
                随机大写, 随机小写, 随机数字
                draw_obj.text((x, y), 生成的随机字符文本内容, 随机三基色, 字体对象)
        6. 使用内存管理器模块. 实现快速生成图片, 再获取二进制的图片数据
            from io import BytesIO
            io_obj = BytesIO()
            image_obj.save(io_obj, '图片格式')
            image_bytes = io_obj.getvalue()
        提示: 随机验证码要保存再session中. 为了在登录认证的时候可以获取到
    """
    # 推导步骤1: 直接获取后端现成的图片二进制数据发送给前端
    # with open(r'static/img/222.png', 'rb') as f:
    # data_bytes = f.read()
    # return HttpResponse(data_bytes)

    # 推导步骤2: 利用pillow模块动态产生图片
    """
    # img_obj = Image.new("RGB", (430, 34), 'red')
    img_obj = Image.new("RGB", (430, 34), get_random())
    # 先将图片对象保存起来
    with open('xxx.png', 'wb') as f:
        img_obj.save(f, 'png')
    
    # 再将图片对象读取出来
    with open('xxx.png', 'rb') as f:
        data_bytes = f.read()
    return HttpResponse(data_bytes)
    """

    # 推导步骤3: 文件存储繁琐IO操作效率低  借助于内存管理器模块
    """
    img_obj = Image.new('RGB', (430, 34), get_random())
    io_obj = BytesIO()  # 生成一个内存管理器对象  你可以看成是文件句柄
    img_obj.save(io_obj, 'png')
    return HttpResponse(io_obj.getvalue())   # 从内存管理器中读取二进制的图片数据返回给前端
    """
    # 最终步骤4: 写图片验证码
    image_obj = Image.new('RGB', (430, 34), get_random())
    draw_obj = ImageDraw.Draw(image_obj)  # 产生一个画笔对象
    font_obj = ImageFont.truetype('static/font/111.ttf', 30)  # 字体样式 大小

    # 随机验证码  五位数的随机验证码  数字 小写字母 大写字母
    code = ''
    for i in range(1, 6):
        upper_symbol = chr(random.randint(65, 90))
        lower_symbol = chr(random.randint(97, 122))
        number = str(random.randint(0, 9))
        # 从上面三个里面随机选择一个
        random_one = random.choice([upper_symbol, lower_symbol, number])
        # 将产生的随机字符串写入到图片上
        """
        为什么一个个写而不是生成好了之后再写
        因为一个个写能够控制每个字体的间隙 而生成好之后再写的话
        间隙就没法控制了
        """
        draw_obj.text((i * 65, 3), random_one, get_random(), font_obj)
        code += random_one
    # 随机验证码在登陆的视图函数里面需要用到 要比对 所以要找地方存起来并且其他视图函数也能拿到
    print(code)
    request.session['code'] = code

    io_obj = BytesIO()
    image_obj.save(io_obj, 'png')
    return HttpResponse(io_obj.getvalue())

1-3. 为验证码图片绑定点击事件, 在点击的时候跟新src属性, 实现点击一次从新发一次get请求, 实现每次地验证码局部地跟新, 不需要每次刷新网页

$('#id_img').click(function () {
    // $(this).attr('src', $oldVal + '?'):  是实现验证码的src属性的跟新操作
    // .parent().prev().children().val(''): 后面的是标签查找到对应输入验证码的input框, 在每刷新验证码, 同步的把之前输入的内容清空.
    let $oldVal = $(this).attr('src');
    $(this).attr('src', $oldVal + '?').parent().prev().children().val('');
});

2. 视图函数中书写登录功能


def login(request):
    """
    1. 先验证验证码
    2. 再验证用户数据
    3. 保存用户状态
    4. 获取到用户没登时输入对应的url, 跳转到登录页面以后, 在登录成功以后直接跳转到用户没登录之前想登录之后访问的页面
    提示: 争对错误的提示信息如果是forms组件效验的错误,  错误信息可以传form_obj.errors
    """
    form_obj = LoginForms()
    if request.is_ajax():
        if request.method == 'POST':
            back_dic = {'code': 1000, 'msg': ''}

            code = request.POST.get('code')
            print('code:', [code])
            # 先验证验证码
            if code.lower() == request.session['code'].lower():
                form_obj = LoginForms(request.POST)
                # 再验证用户数据
                if form_obj.is_valid():
                    cleaned_data = form_obj.cleaned_data
                    print('cleaned_data:', cleaned_data)
                    # 保存用户状态
                    auth.login(request, form_obj.user_obj)

                    # 获取到用户没登时输入对应的url, 跳转到登录页面以后, 在登录成功以后直接跳转到用户没登录之前想登录之后访问的页面
                    target_url = request.GET.get('next')
                    if target_url:
                        back_dic['url'] = target_url
                    else:
                        back_dic['url'] = reverse('app01_home')
                else:
                    print('form_obj.errors:', form_obj.errors)
                    back_dic['code'] = 3000
                    back_dic['msg'] = form_obj.errors
            else:
                back_dic['code'] = 2000
                back_dic['msg'] = '验证码不正确!'
            return JsonResponse(back_dic)

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

3. 移除错误提示信息 和 移除has-error效验的状态

$('input').focus(function () {
    // 第一步是对获取焦点的input框. 移除错误提示信息 和 移除has-error效验的状态
    $(this).next().text('').parent().removeClass('has-error');
    // 第二步是对验证码获取焦点时. 移除验证码的错误信息 和 移除验证码的has-error效验的状态
    $(this).parent().parent().prev().text('').parent().removeClass ('has-error');
})

六. 导航栏搭建

# 用户没有登陆时只展示: 注册  登录
# 用户登陆时展示:
    # 当前登录用户名
        实现: request.user.username
    # 用户的更多操作, 其中包含:
        # 修改密码: 修改密码使用模态框来实现
            展示内容: 当前修改密码用户命(不可选), 原密码, 新密码, 确认密码
                实现: 控制模态框的属性放入修改密码的a标签中
                    data-toggle="modal"
                    data-target="#exampleModal"
                    data-whatever="@mdo"
            使用ajax发送数据:
                用户修改成功以后可以不适用location.href=url跳转到登录页面, 可以使用location.reload()进行页面的刷新.
        # 修改头像
        # 后代管理
        # 退出登录: 退出登录以后就就只显示 注册 登录

# 实现上述2种切换: 通过request.user.is_authenticated() + 模板语法的if判断

七. 页面布局

# 页面布局: 2 8 2布局
    2 2 布局 使用列表展示广告
    8   布局 展示文章内容. 使用媒体对象包裹.
    拓展: 可以使用分页器对文章进行分页.

八. admin后台管理

1. 简介

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

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

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

2. 注册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)

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

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

4. 分析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/

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

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

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

1. 进入文章表
    1-1. 在文章表中创建个人站点
    1-2. 在文章表中创建对应的分类
    提示: 文章的content字段没有指定blank=True, 需要使用任意符号占位. 我们这里用的是: ...
2. 进入用户表添加个人站点与用户表的绑定关系
    注意: 你在创建个人站点的时候, 同时要保证有这个用户(你要提前注册)
3. 进入文章标签表
    先创建标签  再绑定个人站点
4. 进入文章与标签表. 注意: 不要绑错了, 千万不要把自己的文章绑定到别人标签上了.

九. 用户头像展示

# 前言:
    网址所使用的静态文件默认放在static文件夹下
    用户上传的静态文件也应该单独放在某个文件夹下. 我们这里放在media下

# 配置用户上传文件位置: media配置, 配置以后可以让用户上传的所有文件都固定存放在某一个指定的文件夹下
    # 配置用户上传的文件存储位置. 在settings.py中指定
    MEDIA_ROOT = os.path.join(BASE_DIR,'media')  # 文件名随你自己, 它会自动创建多级目录.

# 开设后端指定文件夹资源
    首先你需要自己去urls.py书写固定的代码
    from django.views.static import serve
    from BBS14 import settings

    # 暴露后端指定文件夹资源 固定写法. 注意: media就是你settings.py中指定的路径, 其他的需要一模一样. 这一步后面不要加斜杠了(?P<path>.*)
    url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT})

十. 图片防盗链

思考: 如何避免别的网站直接通过本网站的url访问本网站资源

# 简单的防盗
  我可以做到请求来的时候先看看当前请求是从哪个网站过来的
  如果是本网站那么正常访问
  如果是其他网站直接拒绝
    请求头里面有一个专门记录请求来自于哪个网址的参数
    Referer: http://127.0.0.1:8000/xxx/

# 如何避免
  1. 要么修改请求头referer. 修改成与对方Referer一样.
  2. 直接写爬虫把对方网址的所有资源直接下载到我们自己的服务器上

十一. 个人站点页面搭建

# url分析:
# 个人站点分析
    主要是通过用户名来区分每个不同的用户的个人站点
    https://www.cnblogs.com/yang1333
    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							 2020-11-11					2020-11
    2			222							 2020-11-12					2020-11
    3			333							 2020-11-13					2020-11
    4			444							 2020-11-14					2020-11
    5			555							 2020-11-15					2020-11

    如果出现时区问题报错, 那么到settings.py中修改
    TIME_ZONE = 'Asia/Shanghai'
    USE_TZ = True

十二. 文章详情页

# 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因此需要使用跨表查询

十三. 文章点赞点踩

# 文章内容的拷贝展示
描述: 浏览器上你看到的花里胡哨的页面,内部都是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表中对应的数据

十四. 文章评论

# 处理规则: 先处理根评论, 再处理子评论
# 前端
# 处理根评论逻辑
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页面进行渲染.
# 处理子评论逻辑

十五. 后台管理

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

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

十六. 添加文章

# 添加文章需要注意的问题
    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. 将处理好的结果又转成成网页内容的格式

十七. kindeditor富文本编辑器

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

十八. 修改用户头像

# 争对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()

十九. 目录结构

BBS14/
├── app01
    ├── forms
		├── login_forms.py
		└── register_forms.py 
    ├── media
		├── article_img
		├── avatar
		└── css
    ├── migrations
		├── __init__.py
		└── 001_inital.py 
    ├── templatetags
		└── my_tag.py 
    ├── utils
		└── pager.py 
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── models.py
    ├── tests.py
    └── views.py 
├── app02
├── BBS14
	├── __init__.py
	├── settings.py
	├── urls.py
	└── wsgi.py 
├── static
	├── font
		├── 111.ttf
		├── 222.ttf
		└── 333.ttf 
	├── img
		├── 111.png
		├── 222.png
		├── default.png
		├── downdown.gif
		└── upup.gif
	├── js
		└── my_csrf.js 
	└── kindeditor
├── template
	├── backstage_management
		├── add_article.html
		├── __init__.html
		└── __init__.html
			
	├── comment_model
		├── more_action_model_base.html
		└── show_avatar.html
	├── article_detail.html
	├── base.html
	├── error.html
	├── home.html
	├── left.html
	├── login.html
	├── register.html
	└── site.html
├── manage.py  
├── mysite     
    ├── __init__.py
    ├── settings.py  
    ├── urls.py      
    └── wsgi.py     
posted @ 2020-06-10 02:40  给你加马桶唱疏通  阅读(290)  评论(0编辑  收藏  举报