012_01 BBS博客园项目_Python编写

  ##项目开发流程

#1.项目需求分析
    产品经理+架构师+开发经理/组长 去到客户的公司谈需求(博弈的过程)    
#2.项目架构设计
    架构师设计(数据库(主库:MySQL,从库:redis,mongodb),框架的选择,项目功能划分...) 
        报价:每个开发人员1500~3000     300万
        多部门联合审批    
#3.分组开发
    开会 分任务 
    小组人员开始搬砖 (忘生成好的框架内填写代码即可)
#4.测试
    1.开发人员自己测试(显而易见的bug)
    2.测试人员(妹纸)   
    
#5.交付上线
    1.自己的服务器
    2.对方的服务器

  ##项目表结构设计

#项目最最重要是表结构设计
        如果一张表内的数据分成两块 一块是经常使用的 一块是不经常使用的,这个时候考虑数据优化的问题
        一对一关系
            1.一张表拆成了两张表
            2.两张表中的数据是一一对应的
        
        用户表(利用auth_user表)
            phone
            avatar
            create_time
            blog            一对一个人站点
            
                ps:DateField()
                    auto_now:每次操作数据都会将当前时间更新
                    auto_now_add:自动将创建该数据的时间记录下来之后不再改变
                
        个人站点表
            站点名称 site_name
            站点标题 site_title
            站点样式 site_theme
        
        文章表    
            文章标题 title
            文章简介 desc
            文章内容 content
            发布时间 create_time
            
            blog            一对多个人站点
            category        一对多分类表
            tag             多对多标签表
            
            # 数据库设计优化(******)
            up_num
            down_num
            comment_num 
            # 当你操作点赞点踩表或者评论表的时候 只要保证上面三个同步更新
            
            
        
        标签表
            tag_name
            blog          一对多个人站点
        
        分类表
            category_name
            blog            一对多个人站点
            
        
        点赞点踩表
            user            一对多user表
            article            一对多article表  
            is_up           0/1
        
        文章评论表
            user            一对多user表
            article         一对多article表  
            content         
            create_time
            parent          自关联评论表 (to='self')  # self表示的就是当前表
            
        
        表与表之间关系判断
            表中的一条数据能否对于另外一张表的多条数

  ##表之间的关系

  ##各个功能分析及技术点

#1、forms组件  
    models是models forms是forms 两者没有任何关系
    只是我们认为用forms组件校验models里面的数据
    
    通常是用来校验模型表的数据   ModelForm
        1.渲染标签
        2.校验数据
        3.展示信息
    from django import forms
    class MyForm(forms.Form):
        username = forms.CharField(...)
        
    将数据当做字典传入类中
    is_valid()  校验数据多传不校验少传默认下不行的 
    cleaned_data
    errors
    
    渲染标签三种方式(render渲染)
        form_obj = MyForm()  # get请求和post变量名必须是一样的
        if request.method == "POST":
            form_obj = MyForm(request.POST)
        return render(request,'...html',locals())

        1.{{ form_obj.as_p }}
        2.{{ form_obj.username.label }}{{ form_obj.username }}
        3.{% for foo in form_obj %}
                {{foo.label}}{{foo}}
                {{foo.errors.0}}
            {%endfor%}
    钩子函数
        
    
#2、注册功能
    1.利用forms组件渲染前端页面
        1.局部钩子 校验用户名是否存在
        2.全局钩子 校验密码是否一致
    2.自己写前端获取用户头像
        1.利用label标签能够自动指向指定的input
        2.将img标签放入了label标签中,将默认的type=file框隐藏了
    3.用户选择头像之后动态展示出来
        """
        img标签的src的值
            1.图片的路径地址
            2.图片的二进制数据
            3.后端url
        """
        1.利用ajax给input绑定了一个change事件
        2.利用内置对象FileReader读取文件 
            var fileReader = new FileReader();
            var fileObj = $('input[type="file"]')[0].files[0];
            # 交给文件阅读器读取文件内容
            fileReader.readAsDataURL(fileObj)  # 异步操作
            # 等待文件阅读器读取完毕
            fileReader.onload = function(){
                $('#img_avatar').attr('src',fileReader.result)
            }
        3.利用ajax提交post请求
            1.当用户点击提交按钮你需要做的事
                1.利用form标签的内置的可序列化方法将普通获取用户输入的文本框键值对自动获取出来
                $('#myform').serializeArray()  # 获取到的是一个数组内部是一个个自定义对象
                2.利用each循环循环取出自定义对象里面的键值对(username,password,confirm_password,email,csrfmiddlewaretoken)
                3.既要传文件有要传数据 利用内置的FormData对象
                    循环普通键值对
                    var formData = new FormData();
                    formData.append('','')
                    # 添加文件数据
                    formData.append('my_avatar',$('input[type="file"]')[0].files[0])
                4.发送formdata类型的数据  应该考虑 前后端传输数据的编码格式
                    ps:1.urlencoded(form与ajax默认的提交数据编码格式),2.formdata,3.application/json
                    
                    data:formData
                    contentType:false,
                    processData:false,
                
        4.post请求后端逻辑
            1.将reuqest.POST中的数据直接交给forms组件校验
            2.如果校验通过 cleaned_data   ps:在存储数据的时候models.Userinfo.objects.create_user(username='',password='')
            3.向将多余的键值对的去除 将文件键值对手动添加进去
            4.直接将字典打散传入 
            
            
            5.当数据校验不通过 返回错误信息
                错误信息(form_obj.errors)以json的形式返回给ajax的回调函数
                forms组件在渲染input框的时候 id值有固定格式   id_字段名
            6.回调函数中利用each循环手动拼接input框的id值
                将错误渲染到对应的input下面的span标签中
            7.健壮性功能  用户再次输入的时候 将报错信息移除
                $('input').focus(function(){
                    $(this).next().html('').parent().removeClass('has-error')
                })
        

#4、登陆功能
    1.手动搭建获取用户登录的前端页面 用户名 密码 验证码
    
    2.验证码功能
        ps:利用的是img标签src属性能够支持后端url书写 自动访问该url
        1.直接将后端一个图片以二进制形式发送
        2.利用pillow模块 动态生成图片
            pip3 install pillow
            from PIL import Image,ImageDraw,ImageFont
            # 生成图片Image.new('RGB',(280,40),'red')/Image.new('RGB',(280,40),(255,255,255))
            # 生成画笔对象 将生成好的图片对象传入ImageDraw.draw(img_obj)
            # 生成字体对象 将字体文件(.ttf)和字体大小传入ImageFont.truetype(字体文件,字体大小)
            
            # 如何生成随机验证码
            ps:chr,ord
            import random
            code = ''
            for i in range(5):
                upper_str = chr(random.randint(65,90))
                lower_str = chr(random.randint(97,122))
                random_int = str(random.randint(0,9))
                # 随机获取上面的一个   写到图片上
                res = random.choice([upper_str,lower_str,random_int])
                draw_obj.text((20+i*45,20),res,get_random(),font_obj)
                code += res
            # 需要将验证码存入session中以便后续的校验
            request.session['code'] = code
            """
            1.自动生成一个随机字符串
            2.将键值对存入django自己创建session表中
                ps:django session默认的超时时间 两周 
            3.将生成好随机字符串返回给浏览器保存
            """
            
    3.ajax提交数据
        1 先校验验证码是否一致
        2 在校验用户名和密码是否正确(auth模块自动auth.authenticate(username='',password=''))
        3.登陆成功之后记录用户状态 auth.login(request,user_obj)
            

#5、主页
    1.将网站所有的文章都展示出来
        借助于django admin快速录入数据
            1.createsuperuser
            2.将需要操作的模型表注册到对应的应用下的admin.py文件中
                admin.site.register(models.Userinfo)
            3.录入数据的时候 关系绑定一定要考虑周全
                eg:用户与个人站点你得手动去绑定
    2.网站用的静态的资源默认放在static文件夹下
        用户上传的静态文件应该单独再开一个文件夹存放(media)
    
    3.去settings.py中配置路径
        MEDIA_ROOT = os.path.join(BASE_DIR,'media')
        # 用户上传的文件就会自动方法media文件夹
    
    4.将media文件夹暴露给用户
        手动开路由
        from django.views.static import serve
        from BBS import settings
        url(r'^media/(?P<path>.*)',serve,{'document_root':settins.MEDIA_ROOT})
    5.media配置可以暴露后端任意文件夹中的资源

#6、个人站点
    1.展示的是当前用户自己写的所有文章
    2.侧边栏展示
    
        article_list  # 当前用户所有的文章
        1.文章标签
            # 查询当前用户下的每一个标签及标签下的文章数
            models.Tag.objects.filter(blog=blog).annotate(c=Count('article__id')).values_list('c','tag_name')
        2.文章分类
            # 同上 理
        3.日期归档
            # 参考的是官方文档
    3.侧边栏筛选功能
        1.视图函数 共用site
            url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<params>.*)/',views.site)
        2.site视图函数形参修改
            site(request,username,**kwargs)
        3.基于当前用户所有的文章再做一层筛选 利用queryset对象可以无限制的调用queryset方法
            1.判断kwargs有没有值
            2.获取kwargs里面的值
                condition = kwargs.get('condition')
                params = kwargs.get('params')
            3.判断condition从而决定按照什么条件筛选
                category_id
                tag__id
                基于双下滑线 __year,__month
                
#7、文章详情
    1.模板继承 需要解决侧边栏展示的问题
    2.自定义inclusion_tag
        from django.template import Library
        register = Library()
        
        @register.inclusion_tag('left_menu.html')
        def left_menu(username):
            # 将个人站点视图函数中侧边栏相关代码直接拷贝过来 需要什么补什么
            ...
    3.使用 {% load my_tag %}   {% left_menu username %}
    4.点赞点踩
        1.拷贝博客园样式(html+css)
        2.给赞和踩设置一个公共的类属性 然后给这个类属性绑定点击事件
        3.通过$(this).hasClass()判断用户点击的是赞还是踩
        4.ajax发送
        5.后端接收
            1.is_up是一个字符串形式的js布尔值类型
                你可以利用json直接转成python布尔值
            2.首先判断当前全球是否是ajax全球
            3.判断用户是否登陆request.user.is_authenticated()
            4.判断当前文章是否是当前用户自己写的
                根据前端传过来的id值 查询出对应的文章对象
                文章对象获取用户对象跟request.user比较
            5.判断当前文章是否已经被当前用户点过
                只需要去点赞点踩查数据 如果有值就意味着 点过
                没有 就没有点过
            6.数据库操作
                文章表里面的普通字段
                        根据is_up判断是up_num还是down_num更新
                点赞点踩表记录数据
                        user 
                        article
                        is_up
        6.前端渲染成功或者失败的信息
            如果点赞点踩成功 你应该讲 对应的数字加1  注意数字应该先转成number类型再加一
            如果错误 找到span标签 将信息填入即可
    5.评论
        1.根评论
            1.前端页面搭建(参考博客园样式)
            2.给提交按钮绑点击事件
                1.将用户评论内容 当前文章id 发送给后端
                2.判断是否登陆(login_required)
                    局部和全局配置
                    @login_required(login_url='/login/')
                    LOGIN_URL = '/login/'
                3.判断是否ajax请求
                4.利用事务
                    """事务四大特性:ACID"""
                    from django.db import transaction
                    with transaction.atomic():
                        # 事务操作
            3.渲染评论楼
                render渲染
                    查询当前文章所有的评论 
                    前端循环展示
                        forloop
                            counter0
                            counter
                            first
                            last
                        
                DOM临时渲染
                    利用ecs6新语法 模板字符串
                    var userName = 'jason'
                    `my name is ${userName}`
            
            
            
            
            
        2.子评论
            1.parent字段设置的是null=True 所以在创建数据的时候该字段传None也不会有问题
            2.点击回复按钮要做的事情
                1.将回复按钮对应的评论的人名渲染到textarea中
                    @人名
                2.textarea自动聚焦 并换行
                    $('input').focus()
                ps:将评论对应的用户名和评论id存到了回复按钮
                3.将全局parentID设值
                
            3.每次提交成功成功将parentId再次清空
            4.将子评论中的@人名文本去除
                1.前端利用slice去除
                2.后端
            5.render渲染子评论
                1.判断当前评论对象是否有parant
                2.跨表查询 利用parent跨自己查根评论的用户名
    
#8、后台管理
    1.只有登录的用户才能看到自己的后台管理
    2.罗列出当前用户写的所有的文章
    3.添加文章
        """
        XSS攻击
        文章简介
        """
        1.kindeditor编辑器
        2.直接利用form表单发送post请求
        3.文章标题 文章内容  文章简介
            1.利用bs4模块
            from bs4 import Beautifulsoup
            res = Beautifulsoup(content,'html.parser')
            res = res.find_all()
            for tag in res:
                print(tag.name)
                if tag.name == 'script':
                    tag.decompose()
            desc = res.text[0:150]
            models.Article.objects.create(...content=str(res))
    4.上传图片   去文档里面找指定方法
    
    5.编辑
    6.删除
    
#9、用户头像修改
    如果用queryset的update方法 不会自动拼接avatar前缀
    可以使用对象点的方式修改
功能需求技术点

  ##代码实现

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'm3^_^92%936ajzk9o#+f%(2858aw_yyjimi1#ncr(tghqghdr!'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01.apps.App01Config',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'bbs.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')]
        ,
        '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',
            ],
        },
    },
]

WSGI_APPLICATION = 'bbs.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases

DATABASES = {
    # 'default': {
    #     'ENGINE': 'django.db.backends.sqlite3',
    #     'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    # }
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'bbs',
        'USER':'root',
        'PASSWORD':'123',
        'HOST':'127.0.0.1',
    }
}


# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/

STATIC_URL = '/static/'

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


#auth模块
AUTH_USER_MODEL =  'app01.Userinfo'

#规定用户上传到的静态资源统一放到media文件夹
MEDIA_ROOT = os.path.join(BASE_DIR,'media')


#配置默认的登录地址
LOGIN_URL = '/login/'
settings
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.


#定义用户表,扩展auth模块
class Userinfo(AbstractUser):
    phone = models.CharField(max_length=11,null=True,blank=True)#blank=True是用来admin后台管理 该参数可以为空
    #当用户上传自己的投降的时候 会将用户上传的头像文件自动存入avatar文件夹下
    avatar = models.FileField(upload_to='avatar',default='avatar/default.jpg')
    #创建数据的时候自动添加当前时间,注意auto_now_add(创建时自动创建时间,后续不会自动修改)和auto_now(实时更新)的区别
    create_time = models.DateField(auto_now_add=True)
    #用户表和个人站点表一对一关系
    blog = models.OneToOneField(to='Blog',null=True)

    class Meta:
        verbose_name_plural = '用户表'  #跟数据库无关,仅仅根admin后台相关
#定义个人站点表
class Blog(models.Model):
    # 站点名称
    site_name = models.CharField(max_length=32)
    # 站点标题
    site_title = models.CharField(max_length=32)
    #站点样式:存放css文件路径
    site_theme = models.CharField(max_length=64)

    def __str__(self):
        return self.site_name

    class Meta:
        verbose_name_plural = '个人站点表'  #跟数据库无关,仅仅根admin后台相关
#定义文件标签表
class Tag(models.Model):
    #标题名称
    tag_name = models.CharField(max_length=32)
    # 个人站点外键:一对多
    blog = models.ForeignKey(to='Blog')

    def __str__(self):
        return self.tag_name
    class Meta:
        verbose_name_plural = '标签表'  #跟数据库无关,仅仅根admin后台相关
#定义文章分类表
class Category(models.Model):
    # 分类名称
    category_name = models.CharField(max_length=32)
    # 个人站点外键:一对多
    blog = models.ForeignKey(to='Blog')

    def __str__(self):
        return self.category_name
    class Meta:
        verbose_name_plural = '分类表'  #跟数据库无关,仅仅根admin后台相关
#定义文章表
class Article(models.Model):
    # 文章标题
    title = models.CharField(max_length=32)
    # 文章描述
    desc = models.CharField(max_length=255)
    # 文本内容:存放大段文本
    content = models.TextField()
    create_time = models.DateField(auto_now_add=True)

    # 数据库优化相关字段(不需要跨表查询啦),# 当你操作点赞点踩表或者评论表的时候 只要保证上面三个同步更新
    # 点赞
    up_num = models.IntegerField(default=0)
    # 点踩
    down_num = models.IntegerField(default=0)
    # 评论数
    comment_num = models.IntegerField(default=0)

    # 个人站点表:一对多
    blog = models.ForeignKey(to='Blog',null=True)
    # 分类表:一对多
    category = models.ForeignKey(to='Category',null=True)
    # 标签表:多对多,这里使用到了through属性,因为django中建立多对多表时不需要手动创建,这种是全自动,但是我们该如何给
    # 自动创建的第三张表添加自己需要的字段呢?所以需要到了该属性
    tag = models.ManyToManyField(to='Tag',through='Artical2Tag',through_fields=('article','tag'))

    class Meta:
        verbose_name_plural = '文章'  #跟数据库无关,仅仅根admin后台相关

    def __str__(self):
        return self.title
# 定义多对多表中的第三张表
class Artical2Tag(models.Model):
    article = models.ForeignKey(to='Article')
    tag = models.ForeignKey(to='Tag')



# 定义点赞点踩表
class UpAndDown(models.Model):
    # 外键用户表:一对多
    user = models.ForeignKey(to='Userinfo')
    # 外键文章表:一对多
    article = models.ForeignKey(to='Article')
    # 传布尔类型 数据库里面会存成 0/1
    is_up = models.BooleanField()

    class Meta:
        verbose_name_plural = '点赞点踩表'  #跟数据库无关,仅仅根admin后台相关
# 定义评论表
class Comment(models.Model):
    # 外键用户表:一对多
    user = models.ForeignKey(to='Userinfo')
    # 外键文章表:一对多
    article = models.ForeignKey(to='Article')
    # 评论内容
    content = models.TextField()
    create_time = models.DateField(auto_now_add=True)
    # 自关联,可以指使用'self',更能通俗易懂,也可以用表明
    parent = models.ForeignKey(to='self',null=True)

    class Meta:
        verbose_name_plural = '评论表'  #跟数据库无关,仅仅根admin后台相关
models
from django.conf.urls import url
from django.contrib import admin
from django.views.static import serve
from bbs import settings
from app01 import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^register/', views.register),
    url(r'^login/', views.login),
    url(r'^get_code/', views.get_code),
    url(r'^home/', views.home),

    #用户相关功能
    url(r'^set_password/', views.set_password),
    url(r'^logout/', views.logout),

    #路由相关配置
    url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}),

    url(r'^up_or_down/',views.up_or_down),

    #评论
    url(r'^comment/',views.comment),

    #后台管理
    url(r'^backend/',views.backend),
    url(r'^add_article/',views.add_article),
    url(r'^upload_img/',views.upload_img),

    #用户修改头像
    url(r'^set_avatar/',views.set_avatar),
    #个人站点,正则匹配起别名占位
    url(r'^(?P<username>\w+)/$',views.site),

    #侧边栏帅选功能
    # url(r'^(?P<username>\w+)/(?P<category>category)/(?P<paream>\d+)/',views.site),
    # url(r'^(?P<username>\w+)/(?P<tag>tag)/(?P<paream>\d+)/',views.site),
    # url(r'^(?P<username>\w+)/(?P<archive>archive)/(?P<paream>\w+)/',views.site),
    # 一条路由实现上面三条路由的功能
    #url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<pareams>.*)/',views.site),
    url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<params>.*)/',views.site),

    #文章详情页
    url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/',views.article_detail),


]
url
#forms组件验证模板类

from django import forms
from app01 import models
class RegForm(forms.Form):
    username = forms.CharField(max_length=8,min_length=3,label='用户名',
                               error_messages={
                                   'max_length':'用户最长八位',
                                   'min_length':'用户最短三位',
                                   'required':'用户名不能为空',
                               },widget=forms.widgets.TextInput(attrs={'class':'form-control'})
                               )
    password = forms.CharField(max_length=8, min_length=3, label='密码',
                               error_messages={
                                   'max_length': '密码最长八位',
                                   'min_length': '密码最短三位',
                                   'required': '密码不能为空',
                               }, widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})  #forms.widgets.PasswordInput密码不显示
                               )
    confirm_password = forms.CharField(max_length=8, min_length=3, label='确认密码',
                               error_messages={
                                   'max_length': '密码最长八位',
                                   'min_length': '密码最短三位',
                                   'required': '密码不能为空',
                               }, widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
                               # forms.widgets.PasswordInput密码不显示
                               )
    email = forms.EmailField(label='邮箱',error_messages={
        'invalid':'邮箱格式不正确',
        'required':'邮箱不能为空'
    },widget=forms.widgets.EmailInput(attrs={'class':'form-control'})
                             )
    # 局部钩子  判断当前用户名是否存在
    def clean_username(self):
        #判断当前用户是否存在
        username = self.cleaned_data.get('username')
        user_obj = models.Userinfo.objects.filter(username=username).first()
        if user_obj:
            self.add_error('username','用户已存在')
        else:
            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('confirm_password','两次密码不一致')
        else:
            return self.cleaned_data
myforms
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.Tag)
admin.site.register(models.Category)
admin.site.register(models.Article)
admin.site.register(models.Artical2Tag)
admin.site.register(models.UpAndDown)
admin.site.register(models.Comment)
admin
import json
import random

from django.shortcuts import render,HttpResponse,redirect
from django.utils.safestring import mark_safe

from app01.myforms import RegForm
from app01 import models
from django.http import JsonResponse
from django.contrib import auth
from django.db.models.functions import TruncMonth
from django.db.models import F
from app01.utils.my_page import Pagination
# Create your views here.


def register(request):
    # 当你用ajax进行数据交互 通常 试图函数都会事先定义一个字典
    back_dic = {'code':None,"msg":None}
    form_obj = RegForm()
    if request.method == 'POST':
        #利用forms组件校验普通字段的数据,即把传过来的数据放到ReForm去校验
        form_obj = RegForm(request.POST)
        if form_obj.is_valid():
            # 校验通过,从claend_data拿到所有数据
            clean_data = form_obj.cleaned_data
            """
               clean_data = {'username':'','password':'','confirm_password':'','email':''}
            """
            # 将confirm_password键值对从clean_data去除掉
            clean_data.pop('confirm_password')

            #先获取头像文件对象  用户是否上传
            avatar = request.FILES.get('my_avatar')
            if avatar:
                clean_data['avatar'] = avatar
            """
                clean_data = {'username':'','password':'','email':'','avatar':'文件对象'}
            """
            # print(clean_data)
            #将数据保存到数据库中
            models.Userinfo.objects.create_user(**clean_data)

            back_dic['code'] = 2000
            back_dic['msg'] = '注册成功'

            back_dic['url'] = '/login/'

        else:
            back_dic['code'] = 2001
            back_dic['msg'] = form_obj.errors
        return JsonResponse(back_dic)
    return render(request,'register.html',locals())


def login(request):
    back_dic = {'code':None,"msg":None}
    # request.is_ajax()  # 判断当前请求是ajax请求
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        code = request.POST.get('code')
        # 验证码忽略大小写进行比较
        if request.session.get('code').upper() == code.upper():
            #利用auth认证验证用户信息,因为密码也是密文存入数据库中的
            user_obj = auth.authenticate(username=username,password=password)
            # 类似于但不是等于,注意哈models.Userinfo.objects.filter(username=username,password=password).first()
            if user_obj:
                # print(request.user.is_authenticated())  # 判断当前用户是否登录  这条语句在auth.login之前,结果为False
                auth.login(request,user_obj)
                """
                作用:
                    1. 设置cookie, session,类似于request.session['username'] = user_obj
                    2. 生成request.user的对象, 这个对象可以再视图函数中使用 
                    3. request.user这个对象  相当于 request.session,后续可以在任意位置通过request.user拿到当前登录对象
                """
                back_dic['code'] =100
                back_dic['msg'] = '登录成功'
                back_dic['url'] = '/home/'  #设置一个让前端登陆成功后跳转到home主页
                # print(request.user.is_authenticated())  # 判断当前用户是否登录  这条语句在auth.login之后,结果为True
            else:
                back_dic['code'] = 200
                back_dic['msg'] = '用户名或密码错误'
        else:
            back_dic['code'] = 300
            back_dic['msg'] = '验证码错误'
        return JsonResponse(back_dic)
    return render(request,'login.html')

from PIL import Image,ImageDraw,ImageFont
from io import BytesIO

# 生成三个随机数
def get_random():
    return random.randint(0,255),random.randint(0,255),random.randint(0,255)
def get_code(request):
    # 生成一个图片对象
    img_obj = Image.new('RGB',(260,30),get_random())
    #在当前图片上生成一个画笔
    img_draw = ImageDraw.Draw(img_obj)
    # 设置样式
    img_font = ImageFont.truetype('static/font/222.ttf',30)

    # 图片验证码 (数字 小写字母 大写字母)  五位验证码  1aZd2
    # A-Z:65-90  a-z:97-122
    # 定义一个变量用来存储验证码
    code = ''
    for i in range(5):
        upper_str = chr(random.randint(65,90))
        lower_str = chr(random.randint(97,122))
        random_int = str(random.randint(0,9))
        # 从上面三个里面随机选择一个
        res = random.choice([upper_str,lower_str,random_int])
        #是用画笔对象在生成的图片上写一个一个的验证码,注意书写的坐标位置是在变化的
        img_draw.text((40+ i * 40,-5),res,get_random(),img_font)
        code += res
    #保存到内存管理器中,需要用到BytesIO模块
    # 就把它当成文件句柄
    io_obj = BytesIO()
    img_obj.save(io_obj,'png')# 图片格式必须得传
    #找一个公共的地方存储验证码 以便后续其他视图函数校验
    request.session['code']=code
    #讲些好的图片返回到前端img标签src属性中(再次说一下src属性可以存放三种:1、静态路径地址 2、二进制数据 3、url地址)
    # io_obj.getvalue() 获取二进制数据,
    return HttpResponse(io_obj.getvalue())


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

def set_password(request):
    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')
        if new_password == confirm_password:
            if request.user.check_password(old_password):
                request.user.set_password(new_password)
                request.user.save()
                return redirect('/login/')
    return render(request,'set_password.html')
def logout(request):
    auth.logout(request)
    return redirect('/login/')

from django.db.models import Count
def site(request,username,*args,**kwargs):
    user_obj = models.Userinfo.objects.filter(username=username).first()
    blog = user_obj.blog
    #查询当前用户下对应的文章列表
    article_list = models.Article.objects.filter(blog=blog)# 当前用户所有的文章
    if not user_obj:
        return render(request,'error.html')
    username = user_obj.username
    #判断kwagrs是否有值,如果有值你应该对上面的article_list在做一层筛选
    if kwargs:#两个值# {'condition':'','params':''}
        condition = kwargs.get('condition')
        params = kwargs.get('params')
        print(condition,params)
        if condition == 'category':
            #链式过滤查询,得到分类下对应的文章 注意category_id位文章表中真实存在的字段
            article_list = article_list.filter(category_id=params)
        elif condition == 'tag':
            # 链式过滤查询,得到标签下对应的文章 注意tag__id:有文章表跨表到标签表使用神奇双下划线
            article_list = article_list.filter(tag__id=params)
        elif condition == 'archive':
            # 链式过滤查询,得到年和月下对应的文章 2018-1
            year,month = params.split('-')#解压赋值 需要分别对年和月进行刷选
            article_list = article_list.filter(create_time__year=year,create_time__month=month)


    #下面是侧边栏渲染相关

   #查询当前用户每一个的分类及分类下的文章数,分类查文章:表明小写
    # category_list = models.Category.objects.filter(blog=blog).annotate(c=Count('article')).values('category_name','c')  列表套字典

    # print(category_list) <QuerySet [{'category_name': 'icon分类一', 'c': 0}, {'category_name': 'icon分类二', 'c': 1}, {'category_name': 'icon分类三', 'c': 2}]>
    #列表套元组
    category_list = models.Category.objects.filter(blog=blog).annotate(c=Count('article')).values_list('category_name', 'c','pk')

    #查询当前用户每一个标签下的文章数
    tag_list = models.Tag.objects.filter(blog=blog).annotate(c=Count('article')).values_list('tag_name', 'c','pk')
    # print(tag_list) <QuerySet [('icon标签一', 1), ('icon标签二', 1), ('icon标签三', 1)]>

    #日期归档
    # data_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values('month').annotate(c=Count('pk')).values_list('month','c')
    date_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values(
        'month').annotate(c=Count('pk')).values_list('month', 'c')
    """
    pk:会自动帮你查找到当前表的主键字段
    """
    # print(date_list)
    return  render(request,'site.html',locals())



def article_detail(request,username,article_id):
    #获取具体文章的对象
    article_obj = models.Article.objects.filter(pk=article_id).first()
    blog = article_obj.blog
    #将当前文章的所有评论内容查询出来发给前端渲染
    comment_list  = models.Comment.objects.filter(article=article_obj)

    #分页
    page_obj = Pagination(current_page=request.GET.get('page',1),all_count=comment_list.count())
    page_queryset = comment_list[page_obj.start:page_obj.end]
    return render(request,'article_detail.html',locals())

#点赞点踩
def up_or_down(request):
    back_dic = {"code": None, 'msg': ''}
    #这里可以来个骚操作,判断ajax请求
    if request.is_ajax():
        """
        1.用户是否登陆
        2.有没有点过
        3.自己不能给自己点赞
        4.数据库同步
            article
            upanddown
            数据要同步
        """
        is_up = request.POST.get('is_up')  # 你拿到的是js格式的布尔值 对应到python里面就是字符串
        is_up = json.loads(is_up)  # 利用json模块 将字符串形式转成python里面的布尔值

        article_id = request.POST.get('article_id')
        #1、判断用户是否登陆
        if request.user.is_authenticated():
            #2、判断当前文章是不是用户自己写的,即判断当前文章对象和当前登录用户是否是一个人
            #先拿到当前文章对象
            article_obj = models.Article.objects.filter(pk=article_id).first()
            if article_obj:
                if not article_obj.blog.userinfo.pk == request.user.pk:
                    #3、判断当前文章是否被点过了,即去点赞表查是否被点过了 逆向思维
                    is_click = models.UpAndDown.objects.filter(user=request.user,article=article_obj)
                    if not is_click:
                        #没有点过,记录数据,问题:到底是点赞还是点踩
                        if is_up:#如果是点赞
                            models.Article.objects.filter(pk=article_id).update(up_num = F('up_num')+1)
                        else:
                            models.Article.objects.filter(pk=article_id).update(up_num=F('down_num') + 1)
                        #点赞点踩操作点赞点踩表写入数据
                        models.UpAndDown.objects.create(user=request.user,article=article_obj,is_up=is_up)
                        back_dic['code'] = 2000
                        back_dic['msg'] = '点赞成功' if is_up else '点踩成功'
                    else:
                        back_dic['code'] = 2001
                        back_dic['msg'] = '你已经点过了'
                else:
                    back_dic['code'] = 2002
                    back_dic['msg'] = '你个臭不要脸的 不能给自己点'
            else:
                back_dic['code'] = 2003
                back_dic['msg'] = '未知错误'
        else:
            back_dic['code'] = 2004
            #这里使用后端取消转义字符
            back_dic['msg'] = mark_safe('请先<a href="//login">登录</a>')
        return JsonResponse(back_dic)



"""
事务:ACID
原子性
一致性
隔离性
持久性

将评论内容同步写入到评论表和文章表,这里用到事务的原子性,要么一起成功,要么一起失败
"""
from django.contrib.auth.decorators import login_required
from django.db import transaction  # django中开启事务 需要先倒入该模块
@login_required  #全局登录认证装饰器
def comment(request):
    back_dic = {'code':None,'msg':''}
    if request.is_ajax():
        comment = request.POST.get('comment')
        article_id = request.POST.get('article_id')
        parent_id = request.POST.get('parent_id')
        with transaction.atomic():
            # 在with代码块写的就是一个事务
            # 文章表修改comment_num字段
            models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num')+1)
            # 评论表里面新增数据
            models.Comment.objects.create(user=request.user,article_id=article_id,content=comment,parent_id=parent_id)
            back_dic['code'] = 2000
            back_dic['msg'] = '评论成功'
        return JsonResponse(back_dic)


@login_required
def backend(request):
    article_list = models.Article.objects.filter(blog=request.user.blog)
    # 分页
    page_obj = Pagination(current_page=request.GET.get('page', 1), all_count=article_list.count())
    page_queryset = article_list[page_obj.start:page_obj.end]
    return render(request,'backend/backend.html',locals())

from bs4 import BeautifulSoup
@login_required
def add_article(request):
    if request.method == 'POST':
        title = request.POST.get('title')
        content = request.POST.get('content')

        # 能够帮我拿到当前用户写的所有的标签 将script删除
        res = BeautifulSoup(content,'html.parser')
        #获取所有标签
        tags = res.find_all()
        for tag in tags:
            # 将script全部删除
            if tag.name == 'script':
                # 删除指定的标签
                tag.decompose()
        # 获取用户输入的文本值
        desc = res.text[0:150]
        models.Article.objects.create(title=title,content=str(res),desc=desc,blog=request.user.blog)
        return redirect('/backend/')
    return render(request,'backend/add_article.html')

from bbs import settings
import os
@login_required
def upload_img(request):
    back_dic = {'error': ''}
    if request.method == 'POST':
        # print(request.FILES) 查看字典的key 和文件值
        img_obj = request.FILES.get('imgFile')
        # 规定编辑器上传的图片全部放到media文件夹里面的upload_img文件夹下
        # 1.将文件存储到media文件夹下
        path = os.path.join(settings.BASE_DIR, 'media', 'upload_img')
        if not os.path.exists(path):
            os.mkdir(path)
        file_path = os.path.join(path, img_obj.name)
        with open(file_path, 'wb') as f:
            for line in img_obj:
                f.write(line)
        # 2.将文件路径返回给前端
        back_dic['error'] = 0
        back_dic['url'] = '/media/upload_img/%s' % img_obj.name
        """
        //成功时
        {
                "error" : 0,
                "url" : "http://www.example.com/path/to/file.ext"
        }
        //失败时
        {
                "error" : 1,
                "message" : "错误信息"
        }

        """
    return JsonResponse(back_dic)


#用户修改头像
@login_required
def set_avatar(request):
    username = request.user.username
    if request.method == 'POST':
        new_avatar = request.FILES.get('new_avatar')

        # 如果用queryset对象更新头像 不会自动帮你拼接前缀,要手动拼接
        # models.Userinfo.objects.filter(pk=request.user.pk).update(avatar = new_avatar)
        #或者直接通过对象点属性来进行修改,但是这样的缺点是该对象的整个信息都会从新写一遍
        user_obj = models.Userinfo.objects.filter(pk=request.user.pk).first()
        user_obj.avatar = new_avatar
        user_obj.save()
        return redirect('/home/')
    return render(request,'set_avatar.html',locals())
views
from django.template import Library
from app01 import models
from django.db.models import Count
from django.db.models.functions import TruncMonth
register = Library()

#自定义inclusion_tag
@register.inclusion_tag('left_menu.html')
def left_menu(username):
    blog = models.Blog.objects.filter(site_name=username).first()
    # 列表套元组
    category_list = models.Category.objects.filter(blog=blog).annotate(c=Count('article')).values_list('category_name',
                                                                                                       'c', 'pk')

    # 查询当前用户每一个标签下的文章数
    tag_list = models.Tag.objects.filter(blog=blog).annotate(c=Count('article')).values_list('tag_name', 'c', 'pk')
    # print(tag_list) <QuerySet [('icon标签一', 1), ('icon标签二', 1), ('icon标签三', 1)]>

    # 日期归档
    # data_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values('month').annotate(c=Count('pk')).values_list('month','c')
    date_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values(
        'month').annotate(c=Count('pk')).values_list('month', 'c')
    """
    pk:会自动帮你查找到当前表的主键字段
    """
    # print(date_list)

    return locals()
my_tag include_tag过滤器
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
     <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <link rel="stylesheet" href="/static/bs-3.3.7/css/bootstrap.css">
    <script src="/static/bs-3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
    <div class="row">
        <h1 class="text-center">注册</h1>
        <hr>
        <form action="" id="myform" novalidate>
            {% csrf_token %}
            {% for foo in form_obj %}
                <div class="form-group">
                    <label for="{{ foo.auto_id }}">{{ foo.label }}</label>
                    {{ foo }}
                    <span class="pull-right" style="color: red"></span>
                </div>
            {% endfor %}
            <div class="form-group">
                {#  label标签for属性指向input,点击头像调用input功能#}
                <label for="id_file">头像
                <img src="/static/image/default.jpg" alt="" width="80px" style="margin-left: 20px" id="id_img">
                </label>

                <input type="file" name="myfile" id="id_file" class="hidden">
            </div>
            <input type="button" class="btn btn-success pull-right" value="注册" id="id_submit">
        </form>
    </div>
</div>
</body>
<script>
    $('#id_file').change(function () {
        {#获取文件对象#}
        var fileObj = $(this)[0].files[0];
        {#利用内置对象:文件阅读器FileReader    #}
        var fileReader = new FileReader();
        {#将文件对象交给文件阅读器对象,生成文件对象的二进制数据    #}
        fileReader.readAsDataURL(fileObj); //异步,不会等待加载完毕就继续执行下面的语句
        {#DOM操作修改img标签的src属性值    #}
        fileReader.onload = function () {
            $('#id_img').attr('src',fileReader.result)
        };

    });
    // 点击注册按钮 触发点击事件
    $('#id_submit').click(function () {
        // 利用内置对象FormData完成既有普通键值又有文件数据的发送
        var formData = new FormData()
        // 添加普通键值对
        // console.log($('#myform').serializeArray());  // 会将form标签内 普通的键值对 自动组成一个数组的形式返回给你
        $.each($('#myform').serializeArray(),function (index,obj) {// $.each(你想要被循环的对象,函数(索引,单个单个的对象))
            formData.append(obj.name,obj.value) // 仅仅是将普通的键值对添加进去
        });
        {# 添加文件数据   #}
        formData.append('my_avatar',$('#id_file')[0].files[0])
        //ajax发送数据
        $.ajax({
            url:'',
            type:'post',
            data:formData,
            contentType:false,
            processData: false,
            success:function (data) {
                if (data.code == 2000){
                    window.location.href=data.url
                }else {
                    {#console.log(data.msg)#}
                    $.each(data.msg,function (index,obj) {
                        {#console.log(index,obj)#}
                        //字符串拼接得到input对应的id值,在前端可查到input的id格式位id_username等
                        var targetid = '#id_'+ index;
                        //查找span标签,添加文本信息这里使用html()方法,也可使用text()方法添加
                        $(targetid).next().html(obj[0]).parent().addClass('has-error')
                    })
                }
            }
        })
    })

    // input框获取焦点事件
    $('input').focus(function () {
        // 将当前input所在的div移除has-error属性 并将下面的span标签内的内容也移除了
        $(this).next().html('').parent().removeClass('has-error')
    })
</script>
</html>
register.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <link rel="stylesheet" href="/static/bs-3.3.7/css/bootstrap.css">
    <script src="/static/bs-3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <h1 class="text-center">登录</h1>
            <hr>
            <div class="form-group">
                <label for="id_username">用户名</label>
                <input type="text" name="username" id="id_username" class="form-control">
            </div>
            <div class="form-group">
                <label for="id_password">密码</label>
                <input type="password" name="password" id="id_password" class="form-control">
            </div>
            <div class="form-froup">
                <label for="id_code">验证码</label>
                <div class="row">
                    <div class="col-md-6">
                        <input type="text" name="code" id="id_code" class="form-control">
                    </div>
                    <div class="col-md-6">
                        <img src="/get_code/" alt="" width="260px" height="30px" id="id_img">
                    </div>
                </div>
            </div>
            <br>
            <input type="button" class="btn btn-info" value="登录" id="id_submit">
            <span style="color: red" id="id_errro" class="pull-right"></span>
        </div>

    </div>
</div>
</body>
<script>
    {#点击验证码局部双薪验证码    #}
    $('#id_img').click(function () {
        var oldPath = $(this).attr('src');
        {#方法一:直接在src属性后面添加任意字符,这里添加一个问好#}
        {#$(this).attr('src',oldPath += '?');#}

        // 方法二:优化思路 判断当前url后面是否有问号 有问号就去掉 没有问号就加上
        if( oldPath.includes('?')){
            oldPath.substring(0,oldPath.length-1)
        }else {
            oldPath += '?'
        }
        $(this).attr('src',oldPath)
    })

    $('#id_submit').click(function () {
        $.ajax({
            url:'',
            type:'post',
            data:{
                'username':$('#id_username').val(),
                'password':$('#id_password').val(),
                'code':$('#id_code').val(),
                'csrfmiddlewaretoken':'{{ csrf_token }}'
            },
            success:function (data) {
                if (data.code == 100){
                    {# 登录成功跳转到主页   #}
                    window.location.href = data.url
                }else {
                    $('#id_errro').html(data.msg)
                }
            }
        })
    })

</script>
</html>
login.html
{% extends 'base.html' %}

{% block css %}
    <style>
        #div_digg {
            float: right;
            margin-bottom: 10px;
            margin-right: 30px;
            font-size: 12px;
             128px;
            text-align: center;
            margin-top: 10px;
        }

        .diggit {
            float: left;
             46px;
            height: 52px;
            background: url('/static/image/upup.gif') no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }

        .buryit {
            float: right;
            margin-left: 20px;
             46px;
            height: 52px;
            background: url('/static/image/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: gray;
        }
    </style>
{% endblock %}
{% block content %}
    <h1>{{ article_obj.title }}</h1>
    {# 取消转义   #}
    {{ article_obj.content |safe }}
    {#点赞点踩    #}
    <div class="clearfix">
        <div id="div_digg ">
            <div class="diggit action">
                <span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
            </div>
            <div class="buryit action">
                <span class="burynum" id="bury_count">{{ article_obj.dowm_num }}</span>
            </div>
            <div class="clear"></div>
            <div class="diggword" id="digg_tips">
                <span style="color: red" id="id_info"></span>
            </div>
        </div>
    </div>

    {#评论楼渲染    #}
    {#    #51楼 2018-12-05 15:50 漏网的小鱼  #}
    <h4>评论列表</h4>
    <ul class="list-group">
        {% for comment in page_queryset %}
            <li class="list-group-item">
                <div>
                    <span>#{{ forloop.counter }}楼</span>
                    <span>{{ comment.create_time|date:'Y-m-d' }}</span>
                    <span><a href="/{{ comment.user.username }}/">{{ comment.user.username }}</a></span>
                    <span class="pull-right  reply" username="{{ comment.user.username }}" comment_id="{{ comment.id }}"><a>回复</a></span>
                </div>
                {#                comment.parent判断评论表中的parent字段存不存在#}
                {% if comment.parent %}
                {#                    comment.parent.user.username 从评论表跨到用户表查询根评论的用户名#}
                <p><a href="#">@</a>{{ comment.parent.user.username }}</p>

                {% endif %}
                {{ comment.content }}
            </li>
        {% endfor %}
        {{ page_obj.page_html|safe }}

    </ul>


    {#评论样式#}
    <div>
        <p><span class="glyphicon glyphicon-comment">发表评论</span></p>
        <p>
            昵称:<input type="text" id="tbCommentAuthor" class="author" disabled="disabled" size="50"
                      value="{{ request.user.username }}">
        </p>
        <p>评论内容:</p>
        <p>
            <textarea name="" id="id_comment" cols="60" rows="10"></textarea>
        </p>
        <p>
            <input type="button" class="btn btn-primary" value="提交评论" id="id_submit">
        </p>
    </div>

    <script>
        //点赞点踩
        $('.action').click(function () {
            {#判断是点赞还是点踩的关键代码#}
            var isUp = $(this).hasClass('diggit');
            {#获取点赞和点踩所在的div层,要对数值进行操作#}
            var $spanEle = $(this).children();
            $.ajax({
                url: '/up_or_down/',
                type: 'post',
                data: {'is_up': isUp, 'article_id':{{article_obj.pk}}, 'csrfmiddlewaretoken': '{{ csrf_token }}'},
                success: function (data) {
                    if (data.code == 2000) {
                        {#给span标签设置值#}
                        $('#id_info').html(data.msg);
                        $spanEle.text(Number($spanEle.text()) + 1);
                    } else {
                        {#点赞失败后给span设置值#}
                        $('#id_info').html(data.msg)
                    }
                }
            })
        })
        //评论逻辑
        //定义一个全局的parentid变量名
        parentId = null;

        //点击回复按钮发生了几件事
        //        1.将根评论的人的名字放到了textarea中(@用户名)
        //        2.自动换行
        //        3.textarea自动获取焦点
        //1.在存储数据的时候 应该将@用户名去掉(前端 后端)
        //2.textarea应该清空
        //3.全局的parentid每次发送评论之后应该自动清空



        $('#id_submit').click(function () {
            //获取评论内容
            var comment = $('#id_comment').val();

            if(parentId){
                // 将comment中的@人名清空掉
                // 先获取\n所在的索引值
                var nIndex = comment.indexOf('\n'); // 根据内容查索引
                comment = comment.slice(nIndex)
            }
            $.ajax({
                url: '/comment/',
                type: 'post',
                data: {
                    'comment': comment,
                    'article_id':{{ article_id }},
                    'csrfmiddlewaretoken': '{{ csrf_token }}',
                    'parent_id':parentId
                },
                success: function (data) {
                    if(data.code ==2000){
                        //DOM操作渲染标签
                        //获取用户名
                        var username = '{{ request.user.username }}';
                        //获取评论内容
                        var comment =$('#id_comment').val()
                        //动态生成li标签
                        var tempStr = `
                            <li class="list-group-item">
                            <div>
                            <span class='glyphicon glyphicon-comment'>${username}</span>
                            </div>
                            ${comment}
                            </li>
                        `;
                        // 查找url标签 将上面的字符串追加到ul标签内
                        $('.list-group').append(tempStr)
                        $('#id_comment').val('')
                        parentId = null;
                    }
                }
            })
        })
    
        //点击恢复按钮发生了下面三件事
        $('.reply').click(function () {
            // 获取当前回复按钮所对应的根评论的用户名
            var username = $(this).attr('username');
            //评论内容区域设置@用户名换行,focus自动获取焦点
            $('#id_comment').html('@'+username+'\n').focus();
            //将全局的parentId修改为被评论对象的id,这个id设置在回复按钮的span属性中,为啥呢,因为这里用起来方便
            parentId=$(this).attr('comment_id')
        })
    </script>
{% endblock %}
article_detail.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <link rel="stylesheet" href="/static/bs-3.3.7/css/bootstrap.css">
    <script src="/static/bs-3.3.7/js/bootstrap.min.js"></script>
    <link rel="stylesheet" href="/media/css/{{ blog.site_theme }}">
    <style>
        .d1{
            margin-top: 5px;
        }
    </style>
    {% block css %}

    {% endblock %}
</head>
<body>
<nav class="navbar navbar-inverse">
    <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
                    data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="#">{{ blog.site_title }}</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>#}
{##}
{#            <ul class="nav navbar-nav navbar-right">#}
{#                {% if request.user.is_authenticated %}#}
{#                    <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="/set_password/">修改密码</a></li>#}
{#                            <li><a href="#">修改头像</a></li>#}
{#                            <li role="separator" class="divider"></li>#}
{#                            <li><a href="/logout/">注销</a></li>#}
{#                        </ul>#}
{#                    </li>#}
{#                {% else %}#}
{#                    <li><a href="/login/">登录</a></li>#}
{#                    <li><a href="/register/">注册</a></li>#}
{#                {% endif %}#}
{#            </ul>#}
{#        </div><!-- /.navbar-collapse -->#}
    </div><!-- /.container-fluid -->
</nav>
<div class="container-fluid">
    <div class="row">
        <div class="col-md-3">
            {% load my_tag %}
            {% left_menu username %}
        </div>
        <div class="col-md-9">
            {% block content %}

            {% endblock %}

        </div>

    </div>
</div>
</body>
</html>
base.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Error_404_资源不存在</title>
    <style type="text/css">
    body{margin:8% auto 0;max- 550px; min-height: 200px;padding:10px;font-family: Verdana,Arial,Helvetica,sans-serif;font-size:14px;}p{color:#555;margin:10px 10px;}img {border:0px;}.d{color:#404040;}
    </style>
</head>
<body>
<a href="http://www.cnblogs.com/"><img src="//static.cnblogs.com/images/logo_small.gif" alt="cnblogs"/></a>
<p><b>404.</b> 抱歉! 您访问的资源不存在!</p>
<p class="d">请确认您输入的网址是否正确,如果问题持续存在,请发邮件至contact&#64;cnblogs.com与我们联系。</p>
<p><a href="/home/">返回网站首页</a></p>
</body>
</html>
error.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <link rel="stylesheet" href="/static/bs-3.3.7/css/bootstrap.css">
    <script src="/static/bs-3.3.7/js/bootstrap.min.js"></script>

    <style>
        .d1{
            margin-top: 5px;
        }
    </style>
</head>
<body>
<nav class="navbar navbar-inverse">
    <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
                    data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="#">博客园</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>

            <ul class="nav navbar-nav navbar-right">
                {% if request.user.is_authenticated %}
                    <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="/set_password/">修改密码</a></li>
                            <li><a href="/set_avatar/">修改头像</a></li>
                            <li><a href="/backend/">后台管理</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="/logout/">注销</a></li>
                        </ul>
                    </li>
                {% else %}
                    <li><a href="/login/">登录</a></li>
                    <li><a href="/register/">注册</a></li>
                {% endif %}
            </ul>
        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>
<div class="container-fluid">
    <div class="row">
        <div class="col-md-2">
            <div class="panel panel-primary">
                <div class="panel-heading">
                    <h3 class="panel-title">托儿所</h3>
                </div>
                <div class="panel-body">
                    我的剑就是你的剑
                </div>
            </div>
            <div class="panel panel-info">
                <div class="panel-heading">
                    <h3 class="panel-title">儿童劫</h3>
                </div>
                <div class="panel-body">
                    六一快乐
                </div>
            </div>
            <div class="panel panel-danger">
                <div class="panel-heading">
                    <h3 class="panel-title">皮皮瞎</h3>
                </div>
                <div class="panel-body">
                    无形之刃最为致命
                </div>
            </div>
        </div>
        <div class="col-md-8">
            {% for article in article_list %}
                <div class="media">
                    <h4 class="media-heading"><a href="/{{ article.blog.userinfo.username }}/article/{{ article.pk }}/">{{ article.title }}</a></h4>
                    <div class="media-left">
                        <a href="#">
                            <img class="media-object" src="/media/{{ article.blog.userinfo.avatar }}" alt="..." width="60">
                        </a>
                    </div>
                    <div class="media-body">
                        {{ article.desc }}
                    </div>
                    <dev class="d1 pull-right">
                        {#   MouseDong 发布于 2019-07-24 17:50 评论(0)阅读(128)           #}
                        <span><a href="">{{ article.blog.userinfo.username }}&nbsp;&nbsp;</a></span>
                        <span>发布于&nbsp;&nbsp;{{ article.create_time|date:'Y-m-d' }}&nbsp;&nbsp;</span>
                        <span><span class="glyphicon glyphicon-comment"></span>评论{{ article.comment_num }}&nbsp;&nbsp;</span>
                        <span><span class="glyphicon glyphicon-thumbs-up"></span>点赞{{ article.up_num }}&nbsp;&nbsp;</span>
                    </dev>
                </div>
                 <hr>
            {% endfor %}

        </div>
        <div class="col-md-2">
            <div class="panel panel-primary">
                <div class="panel-heading">
                    <h3 class="panel-title">贾科斯</h3>
                </div>
                <div class="panel-body">
                    你太小看我啦
                </div>
            </div>
            <div class="panel panel-info">
                <div class="panel-heading">
                    <h3 class="panel-title">草丛伦</h3>
                </div>
                <div class="panel-body">
                    我的大刀已经饥渴难耐
                </div>
            </div>
            <div class="panel panel-danger">
                <div class="panel-heading">
                    <h3 class="panel-title">德邦</h3>
                </div>
                <div class="panel-body">
                    看我的
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>
home.html
<div class="panel panel-primary">
                <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.2 }}">{{ category.0 }}</a>({{ category.1 }})</p>
                    {% endfor %}

                </div>
            </div>
<div class="panel panel-info">
    <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 }}">{{ tag.0 }}</a>({{ tag.1 }})</p>
        {% endfor %}

    </div>
</div>
<div class="panel panel-danger">
                <div class="panel-heading">
                    <h3 class="panel-title">日期归档</h3>
                </div>
                <div class="panel-body">
                        {% for date in date_list %}
                            <p><a href="/{{ username }}/archive/{{ date.0|date:'Y-m' }}/">{{ date.0|date:'Y年m月'}}</a>({{ date.1 }})</p>
                        {% endfor %}
                </div>
            </div>
left_menu.html
{% extends 'base.html' %}

{% block content %}
    <h2 class="text-center">修改头像</h2>
    <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {#显示用户当前头像 ,头像路径在数据库中存在形式为:avatar/2.jpg  ,所以直接获取就可以不需要在拼接全路径     #}
        <img src="/media/{{ request.user.avatar }}" alt="">
        <p>
            <label for="d1">选择头像:<input type="file" id="d1" name="new_avatar"></label>
        </p>
        <input type="submit" class="btn btn-success" value="修改">
    </form>
{% endblock %}
set_avatar.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <link rel="stylesheet" href="/static/bs-3.3.7/css/bootstrap.css">
    <script src="/static/bs-3.3.7/js/bootstrap.min.js"></script>
</head>
<body>


<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <h1 class="text-center">修改密码</h1>
            <hr>
            <form action="" method="post">
                {% csrf_token %}
                <div class="form-group">
                    <label for="id_username">用户名</label>
                    <input type="text" name="username" id="id_username" class="form-control"
                           value="{{ request.user.username }}" disabled>
                </div>
                <div class="form-group">
                    <label for="id_password">原密码</label>
                    <input type="password" name="old_password" id="id_password" class="form-control">
                </div>
                <div class="form-group">
                    <label for="id_new_password">新密码</label>
                    <input type="password" name="new_password" id="id_new_password" class="form-control">
                </div>
                <div class="form-group">
                    <label for="id_confirm_password">确认密码</label>
                    <input type="password" name="confirm_password" id="id_confirm_password" class="form-control">
                </div>
                <br>
                <input type="submit" class="btn btn-info" value="提交" id="id_submit">
                <span style="color: red" id="id_errro" class="pull-right"></span>
            </form>
        </div>

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

</html>
set_password.html
{% extends 'base.html' %}

{% block content %}
     {% for article in article_list %}
                <div class="media">
                    <h4 class="media-heading"><a href="/{{ username }}/article/{{ article.pk }}">{{ article.title }}</a></h4>
                    <div class="media-left">
                        <a href="#">
                            <img class="media-object" src="/media/{{ article.blog.userinfo.avatar }}" alt="..." width="60">
                        </a>
                    </div>
                    <div class="media-body">
                        {{ article.desc }}
                    </div>
                    <dev class="d1 pull-right">
                        {#   MouseDong 发布于 2019-07-24 17:50 评论(0)阅读(128)           #}
                        <span><a href="">{{ article.blog.userinfo.username }}&nbsp;&nbsp;</a></span>
                        <span>@{{ article.create_time|date:'Y-m-d' }}&nbsp;&nbsp;</span>
                        <span><span class="glyphicon glyphicon-comment"></span>评论{{ article.comment_num }}&nbsp;&nbsp;</span>
                        <span><span class="glyphicon glyphicon-thumbs-up"></span>点赞{{ article.up_num }}&nbsp;&nbsp;</span>
                        <span><a href="">编辑</a></span>
                    </dev>
                </div>
                 <hr>
            {% endfor %}
{% endblock %}
site.html
{% extends 'backend/backendbase.html' %}
{% block content %}
    <h3>文章标题</h3>
    <hr>
    <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <p>标题</p>
        <input type="text" name="title" id="id_title" class="form-control">
        <p>内容(kindeditor编辑器,支持拖放/粘贴上传图片)</p>
        <p>
            <textarea name="content" id="id_content" cols="30" rows="10"></textarea>
        </p>
        <input type="submit" value="发布" class="btn btn-danger">
    </form>
    <script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js"></script>
    <script>
        KindEditor.ready(function (K) {
            window.editor = K.create('#id_content', {
                 '100%',
                height:'450px',
                {#0 表示不能拖拽#}
                resizeType:0,
                // 控制文件上传的位置
                uploadJson : '/upload_img/',
                 extraFileUploadParams : {
                        'csrfmiddlewaretoken':'{{ csrf_token }}'

                }
            });
        });
    </script>
{% endblock %}
add_article.html
{% extends 'backend/backendbase.html' %}

{% block content %}
    <table class="table table-hover table-striped">
        <thead>
        <tr>
            <th>标题</th>
            <th>评论数</th>
            <th>点赞数</th>
            <th>点踩数</th>
            <th>操作</th>
            <th>操作</th>
        </tr>
        </thead>
        <tbody>
        {% for article in page_queryset %}
            <tr>
                <td><a href="/{{ request.user.username }}/article/{{ article.pk }}/">{{ article.title }}</a></td>
                <td>{{ article.comment_num }}</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>
    {{ page_obj.page_html|safe }}
{% endblock %}
backend.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Title</title>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <link rel="stylesheet" href="/static/bs-3.3.7/css/bootstrap.css">
    <script src="/static/bs-3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<nav class="navbar navbar-inverse">
    <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
                    data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="#">后台管理</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 navbar-right">
                <li><a href="#">{{ request.user.username }}</a></li>
            </ul>
        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>
<div class="container-fluid">
    <div class="row">
        <div class="col-md-2">
            <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                <div class="panel panel-default">
                    <div class="panel-heading" role="tab" id="headingOne">
                        <h4 class="panel-title">
                            <a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne"
                               aria-expanded="true" aria-controls="collapseOne">
                                操作
                            </a>
                        </h4>
                    </div>
                    <div id="collapseOne" class="panel-collapse collapse in" role="tabpanel"
                         aria-labelledby="headingOne">
                        <div class="panel-body">
                            <a href="/add_article/">添加文章</a>
                        </div>
                    </div>
                    <div id="collapseOne" class="panel-collapse collapse in" role="tabpanel"
                         aria-labelledby="headingOne">
                        <div class="panel-body">
                            <a href="#">添加随笔</a>
                        </div>
                    </div>
                    <div id="collapseOne" class="panel-collapse collapse in" role="tabpanel"
                         aria-labelledby="headingOne">
                        <div class="panel-body">
                            <a href="#">个人设计</a>
                        </div>
                    </div>
                    <div id="collapseOne" class="panel-collapse collapse in" role="tabpanel"
                         aria-labelledby="headingOne">
                        <div class="panel-body">
                            <a href="#">重金求子</a>
                        </div>
                    </div>
                </div>
            </div>

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

                <!-- Nav tabs -->
                <ul class="nav nav-tabs" role="tablist">
                    <li role="presentation" class="active"><a href="#home" aria-controls="home" role="tab"
                                                              data-toggle="tab">文章</a></li>
                    <li role="presentation"><a href="#profile" aria-controls="profile" role="tab"
                                               data-toggle="tab">随笔</a></li>
                    <li role="presentation"><a href="#messages" aria-controls="messages" role="tab"
                                               data-toggle="tab">设置</a></li>
                    <li role="presentation"><a href="#settings" aria-controls="settings" role="tab"
                                               data-toggle="tab">更多</a></li>
                </ul>

                <!-- Tab panes -->
                <div class="tab-content">
                    <div role="tabpanel" class="tab-pane active" id="home">
                        {% block content %}
                        
                        {% endblock %}
                    </div>
                    <div role="tabpanel" class="tab-pane" id="profile">随笔页面</div>
                    <div role="tabpanel" class="tab-pane" id="messages">设置页面</div>
                    <div role="tabpanel" class="tab-pane" id="settings">更多页面</div>
                </div>

            </div>
        </div>
    </div>
</div>
</body>
</html>
backendbase.html

  ##源码地址(百度云)

原文地址:https://www.cnblogs.com/abdm-989/p/11805615.html