• 一个论坛引发的血案


          去病率兵十万深入敌后痛击匈奴,经此一役,胡虏元气甚伤,单于伊治斜弃河西平原而迁王庭至漠北。汉乃设酒泉、张掖、敦煌、武威河西四郡。众将凯旋回师,帝召大将军卫青、骠骑将军霍去病、飞将军李广、苏建、公孙敖、公孙贺等将议于宣室殿。众将皆以需涉荒漠为困而欲弃之。然武帝废胡之心愈坚,怒曰:汝皆以为不可,而胡虏亦曰寡人不可为之,然朕必举全国之力誓击之,永绝胡虏之患!

          遂点精骑十万,以大将军五万兵出定襄,骠骑将军五万出代郡,辎重步卒五十万,挥师远征漠北!

    一、兵马未动,辎重先行。——表结构的设计

    (参照抽屉新热榜)

    在论坛中文章包含的属性(数据库中的字段)有标题、简介、板块、内容、作者、发布时间、最后一次修改时间、优先级(数字权重)。

    model里的字段类型

    auto_now和auto_now_add区别:

    auto_now:记录字段每次被修改的时间

    auto_now_add:记录字段被创建时的时间

    若字段被设置为这两种类型的一种,则字段不可再更改。

    models.py

    #! _*_ coding:utf-8 _*_

    from
    __future__ import unicode_literals from django.db import models from django.contrib.auth.models import User #倒入django-admin自带的User表 from django.core.exceptions import ValidationError #页面提示错误信息 import datetime # Create your models here. #文章 class Article(models.Model): title = models.CharField(max_length=255) #标题 brief = models.CharField(blank=True,null=True,max_length=255)#文章简介,可以为空 category = models.ForeignKey('Category')#板块,需要关联Cotegory类(若要关联尚未定义的model,需要使用引号) content = models.TextField(u'文章内容')#文章内容 author = models.ForeignKey('UserInfo')#作者 pub_date = models.DateTimeField(blank=True,null=True)#发布日期,若是草稿,则没有发布时间 last_modify = models.DateTimeField(auto_now=True)#最后一此修改时间 priority = models.IntegerField(u'优先级',default=1000)#优先级,默认1000
    head_img = models.ImageField('文章标题图片',upload_to='uploads')#存储标题图片,图片上传到uploads目录下,该目录在app下会自动创建
        status_choices = (
            ('draft','草稿'),
            ('published','已发布'),
            ('hidden','隐藏'),
        )
        status = models.CharField(choices=status_choices,default='published',max_length=32)#文章状态
    
        #发布时间的判断
        def clean(self):
            if self.status == 'draft' and self.pub_date is not None:
                raise ValidationError('草稿没有发布时间')
            #如果已经发布,则发布时间设为当前时间
            if self.status == 'published' and self.pub_date is None:
                self.pub_date = datetime.date.today()
    
        def __unicode__(self):
            return self.title
    
        class Meta:
            verbose_name_plural = '文章'
    
    
    #评论
    class Comment(models.Model):
        article = models.ForeignKey(Article,verbose_name='所属文章')#评论所属文章
        parent_comment = models.ForeignKey('self',related_name='my_children',blank=True,null=True)#存储父评论,与自己关联,relate_name获取自己的子评论
        comment_choices = (
            (1,'评论'),
            (2,'点赞'),
        )
        comment_type = models.IntegerField(choices=comment_choices,default=1)#选择评论还是点赞,默认是评论
        user = models.ForeignKey('UserInfo')#发表评论或点赞的用户
        date = models.DateTimeField(auto_now_add=True)#评论时间
        comment = models.TextField(blank=True,null=True)#评论内容
    
        #判断评论是否为空
        def clean(self):
            if self.comment_type == 1 and len(self.comment) == 0:
                raise ValidationError('评论不能为空')
    
        def __unicode__(self):
            return '%s,P:%s,%s' %(self.article,self.parent_comment,self.comment)
        class Meta:
            verbose_name_plural = '评论'
    
    #板块
    class Category(models.Model):
        name = models.CharField(max_length=64)
        brief = models.CharField(blank=True,null=True,max_length=255)#板块简介
        set_as_top_menu = models.BooleanField(default=False)#是否将该模块添加到顶部菜单栏
        position = models.SmallIntegerField()#若在顶部菜单栏显示,则需要设置显示的顺序
        admin = models.ManyToManyField('UserInfo',blank=True)#板块管理员
    
        def __unicode__(self):
            return self.name
    
        class Meta:
            verbose_name_plural = '板块'
    #用户
    class UserInfo(models.Model):
        user = models.OneToOneField(User)#关联到django-admin自带的user表
        name = models.CharField(max_length=32)
        signature = models.CharField(max_length=255,blank=True,null=True)#个性签名
        head_img = models.ImageField(height_field=150,width_field=150,blank=True,null=True)#头像
    
        def __unicode__(self):
            return self.name
    
        class Meta:
            verbose_name_plural = '用户'

    设置settings.py 

    #注册app
    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'blog',
    ]
    
    #设置数据库
    DATABASES = {
        'default': {
            #'ENGINE': 'django.db.backends.sqlite3',
            #'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
            'ENGINE': 'django.db.backends.mysql',
            'NAME':'blog',
            'HOST':'',
            'PORT':'',
            'USER':'root',
            'PASSWORD':'0711'
        }
    }
    
    #模版路径
    'DIRS': [
                os.path.join(BASE_DIR,'templates'),
            ],
    
    #静态文件
    STATICFILES_DIRS=(
        os.path.join(BASE_DIR,'statics'),     #该定义要求statics目录在project目录下
        )

    一切前期准备就绪,在数据库中创建blog库(注意字符编码):

    mysql> create database blog character set utf8;

    同步数据库,创建表:

    python manage.py makemigrations
    
    python manage.py migrate

    注册admin后台

    admin.py

    from django.contrib import admin
    
    from blog import models
    
    # Register your models here.
    
    class ArticleAdmin(admin.ModelAdmin):
        list_display = ('title','category','author','pub_date','last_modify','status')  #每个表在页面上展示的字段
    
    
    class CommentAdmin(admin.ModelAdmin):
        list_display = ('article','parent_comment','comment_type','comment','user','date')
    
    class CategoryAdmin(admin.ModelAdmin):
        list_display = ('name','set_as_top_menu','position')
    
    admin.site.register(models.Article,ArticleAdmin)
    admin.site.register(models.Category,CategoryAdmin)
    admin.site.register(models.Comment,CommentAdmin)
    admin.site.register(models.UserInfo)

    二、华丽铠甲,折戟断箭。——前端展示模版的选择

     Bootstrap中挑选合适的页面模版,全部下载到本地。

    设置模版和静态文件:

    在project目录下创建templates和statics目录分别存放模版文件和静态文件

    settings.py

    #templates
    
        'DIRS': [
            os.path.join(BASE_DIR,'templates'),
        ],
    
    
    #statics STATIC_URL
    = '/static/' #该设置对static目录在app目录下有效 STATICFILES_DIRS =( os.path.join(BASE_DIR,'statics'), #该设置对statics目录在project目录下有效 )

    在statics目录中添加静态文件:

    在templates目录中添加模版文件:

    将下载的html文件修改为base.html作为父模版,和下载的目录一起放在templates下创建的blog目录下,然后创建index.html作为项目首页。

    index.html:

    {% extends 'blog/base.html' %}    #继承父模版base.html

    将base.html中js和css部分的url修改为本地的文件资源,如:

        <!-- Bootstrap core CSS -->
        <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    
        <!-- Custom styles for this template -->
        <link href="/static/bootstrap/css/navbar-fixed-top.css" rel="stylesheet">

    urls.py

    url采用根据不同项目做分发的规则,project目录下的urls.py:

    from django.conf.urls import url,include
    from django.contrib import admin
    
    
    urlpatterns = [
        url(r'^admin/', admin.site.urls),
        url(r'^blog/$',include('blog.urls')),
    ]

    在app中新创建urls.py,然后设置如下:

    from blog import views
    
    
    urlpatterns = [
        url(r'^$',views.index),
    ]

    url设置完毕,创建试图函数index,views.py:

    def index(request):
        return render(request,'blog/index.html')

     一个简单的页面套用完毕。

     

    三、临阵布局,指挥若定。——动态导航栏及登录

    对页面进行改造,将导航栏设为动态,并添加登陆与退出功能。

    1、动态导航

    根据表结构的设计,在category表中,若set_as_top_menu字段为True,则该板块名称即可显示在导航栏中。即每个现实在导航栏中的category,其set_as_top_menu字段必须为Ture。并且,每个category都有一个position标示,这样就可以根据前端请求的url中的id来现实对应的目录名称。

    blog/urls.py:

    from blog import views
    
    
    urlpatterns = [
        url(r'category/(d+)',views.category),
    ]

    views中,根据url中的id,查询category表,并将查询到的category名字返回给前端。

    views.py:

    category_list = models.Category.objects.filter(set_as_top_menu=True).order_by('position')      #将其定义为全局变量,便于多个函数引用
    
    def index(request):
        return render(request,'blog/index.html',{'category_list':category_list})
    
    def category(request,id):
        category_obj = models.Category.objects.get(id = id)
        return render(request,'blog/index.html',{'category_list':category_list,'category_obj':category_obj})

    前端页面base.html中,循环所有在导航栏中显示的category,并和当前请求的category比较(根据id值),若两者相等,则跳转url并加底色。

    base.html:

    {% for category in category_list %}
    {% if category.id == category_obj.id %} #当前请求id等于导航栏中某一个category的id值
    <li class="active"><a href="/blog/category/{{ category.id }}">{{ category.name }}</a></li>
    {% else %}
    <li class=""><a href="/blog/category/{{ category.id }}">{{ category.name }}</a></li>
    {% endif %}
    {% endfor %}

    这样,若点击导航栏中id为3的category,则url跳转至 http://localhost:8000/blog/category/3

    2、登录及退出

    若未登录,主页面右上角显示登录/注册按钮,可跳转至登录或注册页面;若已登录,则在该处显示用户名。

    全局urls.py的设置:

    from blog import views
    
    urlpatterns = [
        url(r'^admin/', admin.site.urls),
        url(r'^blog/',include('blog.urls')),
        url(r'^login/$',views.user_login,name='login'),
        url(r'^logout/$',views.user_logout,name='logout'),
    ]

     分别对登录和退出添加别名,方便前端页面中调用。

    views.py中,引用django框架中的用户登录验证及退出功能。

    from django.shortcuts import render,HttpResponseRedirect
    
    # Create your views here.
    
    from blog import models
    from django.contrib.auth import login,logout,authenticate
    from django.contrib.auth.decorators import login_required
    
    category_list = models.Category.objects.filter(set_as_top_menu=True).order_by('position')
    
    def index(request):
        return render(request,'blog/index.html',{'category_list':category_list})
    
    def category(request,id):
        category_obj = models.Category.objects.get(id = id)
        return render(request,'blog/index.html',{'category_list':category_list,'category_obj':category_obj})
    
    def user_login(request):
        if request.method == 'POST':
            user = authenticate(username = request.POST.get('username'),password = request.POST.get('password'))
            if user is not None:     #登录成功
                login(request,user)
                return HttpResponseRedirect('/blog')  
            else:
                login_error = 'wrong username or password!'
                return render(request,'blog/login.html',{'login_error':login_error})
        return render(request,'blog/login.html')
    
    def user_logout(request):
        logout(request)
        return HttpResponseRedirect('/blog')

    登录页面login.html:

    {% extends 'blog/base.html' %}
    
    {% block page-container %}
        <form action="" method="post"> {% csrf_token %}
            <div>
                <input type="text" name="username">
            </div>
            <div>
                <input type="password" name="password">
            </div>
            <div>
                <input type="submit" value="login">
            </div>
        </form>
        <div>
            {% if login_error %}
                <p style="color: red">{{ login_error }}</p>
            {% endif %}
        </div>
    {% endblock %}

    继承base.html 父模版,自定内容主题。

    base.html中,在右上角添加登陆/注册按钮

    
    
    <ul class="nav navbar-nav navbar-right">
    {% if request.user.is_authenticated %}
    <li class="active"><a href="#">{{ request.user.userinfo.name }}</a></li> #反向查询,查询 UserInfo表中的name字段,这里需要将表名小写
    <li class="active"><a href="{% url 'logout' %}">注销</a></li>
    {% else %}
    <li class=""><a href="{% url 'login' %}">登陆/注册</a></li>
    {% endif %}
    </ul>
     

    效果:

     

    三、左迂回,右包抄,稳坐中军帐。——内容排版展示

    显示文章及图片

    主页及各板块下显示相应文章及图片

    views.py

    category_list = models.Category.objects.filter(set_as_top_menu=True).order_by('position')
    
    def index(request):
        #获取position为1的category,category_obj.id即position为1的categoryid值
        category_obj = models.Category.objects.get(position = 1)
        '''
        返回position为1的category
        前端请求主页时,首先从position=1开始循环所有category列表,此时'全部'板块的position为1,将该category加底色
        '''
        article_list = models.Article.objects.all()   #所有文章
        return render(request,'blog/index.html',{'category_list':category_list,'article_list':article_list,'category_obj':category_obj})
    
    def category(request,id):
        category_obj = models.Category.objects.get(id = id)
        if  category_obj.position == 1:      #如果是全部,则显示全部文章
            article_list = models.Article.objects.all()
        else:
            article_list = models.Article.objects.filter(category_id=category_obj.id)   #article表中category_id字段
        return render(request,'blog/index.html',{'category_list':category_list,'category_obj':category_obj,'article_list':article_list})

    前端页面展示设置中关于图片展示的问题:

    '''
    图片上传到uploads目录,前端请求的url为'/static/uploads/3_DDWq8O3.jpg',
    uploads目录不在django静态文件的配置中,前端不能访问,因此要在setting中添加该目录,
    这样实际图片位置为'/static/3_DDWq8O3.jpg',
    自定义模版,将'/static/uploads/3_DDWq8O3.jpg'变为/static/3_DDWq8O3.jpg'
    前端中图片src为'<img src="/static/{{ article.head_img }}">',
    这样,只需要将'uploads/3_DDWq8O3.jpg'变为'3_DDWq8O3.jpg'即可
    '''

    settings.py:

    STATICFILES_DIRS =(
        os.path.join(BASE_DIR,'statics'),
        os.path.join(BASE_DIR,'uploads'),
        )

    自定义django模版:

    blog目录下创建templatetags/custom.py文件,并在该目录下创建__init__.py文件。

    customer.py:

    #! _*_coding:utf-8 _*_
    from django import template
    
    register = template.Library()
    
    @register.filter
    def truncate_url(img_obj):
        return img_obj.name.split('/',1)[-1]   
      #
    return img_obj.name.split('/')[1]
      #截取结果:['uploads','xxx.jpg']

    #即将uploads/xxx.jpg 截为xxx.jpg,传给前端之后,就是static/xx.jpg #split(
    '/',1)表示只截取一次,如'1/2/3'截取后为['1','2/3']

    index.html页面:

    {% extends 'blog/base.html' %}
    {% load custom %}
    
    {% block page-container %}
        {% for article in article_list %}
            <div class="article-box">
                <div class="article-head-img">
                    <img src="/static/{{ article.head_img | truncate_url}}">   #自定义模版,将articel.head_img交给truncate_url函数处理
     </div> <div class="article-bruff"> {{ article.title }} </div> </div> {% endfor %} {% endblock %}

    显示评论及点赞数

    (样式参考虎嗅)

    statics/bootstrap/css/目录下创建custom.css,用来定义图片及标题显示样式

    custom.css

    .page-container{
        border: 1px dashed darkcyan;
        padding-right: 150px;
        padding-left: 150px;
    }
    
    
    /*去除漂浮*/
    .clear-both{
        clear: both;
    }
    
    
    /*左漂浮*/
    .wrap-left{
        width: 75%;
        float: left;
    
    }
    
    
    /*右漂浮*/
    .wrap-right{
        width: 25%;
        float: right;
        background-color: #9d9d9d;
    }
    
    .footer{
        height: 300px;
        background-color: #ac2925;
    }
    
    /*压缩图片*/
    .article-head-img img{
        height: 150px;
        width: 200px;
    }
    
    /*每条新闻间距*/
    .article-box{
        padding-bottom: 10px;
    }
    
    /*图片和标题之间距离*/
    .article-brief{
        margin-left: -10px;
    }
    
    
    /*文章标题*/
    .article-title{
        font-size: 20px;
    }
    
    
    /*a标签去掉下划线*/
    a:hover{
        text-decoration: none;
    }
    
    
    /*字体样式*/
    .body{
        color: rgb(51, 51, 51);
        font-family: Arial, 微软雅黑, "Microsoft yahei", "Hiragino Sans GB", "冬青黑体简体中文 w3", "Microsoft Yahei", "Hiragino Sans GB", "冬青黑体简体中文 w3", STXihei, 华文细黑, SimSun, 宋体, Heiti, 黑体, sans-serif;
    }
    
    
    /*简介样式*/
    .article-brief-text{
        margin-top: 8px;
        color: #999;
    }
    View Code

    修改base.html主题内容,引用custom.css中定义的样式

    base.html

    <!DOCTYPE html>
    <!-- saved from url=(0048)http://v3.bootcss.com/examples/navbar-fixed-top/ -->
    <html lang="zh-CN"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
        <meta name="description" content="">
        <meta name="author" content="">
        <link rel="icon" href="http://v3.bootcss.com/favicon.ico">
    
        <title>文艺社区</title>
    
        <!-- Bootstrap core CSS -->
        <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    
        <!-- Custom styles for this template -->
        <link href="/static/bootstrap/css/navbar-fixed-top.css" rel="stylesheet">
        <link href="/static/bootstrap/css/custom.css" rel="stylesheet">   <!--引用自定义的css样式文件-->
      </head>
    
      <body>
    
        <!-- Fixed navbar -->
        <nav class="navbar navbar-default navbar-fixed-top">
          <div class="container">
            <div class="navbar-header">
              <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                <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="http://v3.bootcss.com/examples/navbar-fixed-top/#">文艺社区</a>
            </div>
            <div id="navbar" class="navbar-collapse collapse">
              {% block page-menu %}
              <ul class="nav navbar-nav">
                {% for category in category_list %}    <!--循环所有category-->
                    {% if category.id == category_obj.id %}  <!--前端点击的一个category与所有category列表中某一个相等-->
                        <li class="active"><a href="/blog/category/{{ category.id }}">{{ category.name }}</a></li>
                    {% else %}
                        <li class=""><a href="/blog/category/{{ category.id }}">{{ category.name }}</a></li>
                    {% endif %}
                {% endfor %}
              </ul>
              {% endblock %}
              <ul class="nav navbar-nav navbar-right">
                {% if request.user.is_authenticated %}
                    <li class="active"><a href="#">{{ request.user.userinfo.name }}</a></li>
                    <li class="active"><a href="{% url 'logout' %}">注销</a></li>
                {% else %}
                    <li class=""><a href="{% url 'login' %}">登陆/注册</a></li>
                {% endif %}
              </ul>
            </div><!--/.nav-collapse -->
          </div>
        </nav>
    
        <div class="page-container">  <!--内容部分-->
          {% block page-container %}
          <!-- Main component for a primary marketing message or call to action -->
          <div class="jumbotron">
            <h1>Welcome</h1>
            <p>
              <a class="btn btn-lg btn-primary" href="http://v3.bootcss.com/components/#navbar" role="button">View navbar docs »</a>
            </p>
          </div>
          {% endblock %}
        </div> <!-- /container -->
    
        <footer class="footer"></footer>
        <!-- Bootstrap core JavaScript
        ================================================== -->
        <!-- Placed at the end of the document so the pages load faster -->
        <script src="/static/bootstrap/js/jquery-2.2.3.js"></script>
        <script src="/static/bootstrap/js/bootstrap.min.js"></script>
    
    </body></html>
    View Code

    index.html 

    {% extends 'blog/base.html' %}
    {% load custom %}
        {% block page-container %}
        <div class="wrap-left">
            {% for article in article_list %}
                <div class="article-box row">
                    <div class="article-head-img col-lg-4">  <!--图片在左-->
                        <img src="/static/{{ article.head_img | truncate_url}}">  <!--图片展示地址-->
                    </div>
                    <div class="article-brief col-lg-8">    <!--标题在右-->
                        <a class="article-title" href="#">{{ article.title }}</a>  <!--文章标题-->
                        <div class="article-title-info">
                            <span>{{ article.author.name }}</span>
                            <span>{{ article.pub_date }}</span>
                            <span>{% filter_comment article as comments %}</span>
                            <span class="glyphicon glyphicon-comment" aria-hidden="true"></span> <!--评论图表-->
                            <span>{{ comments.comment_count }}</span>  <!--评论数-->
                            <span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span> <!--点赞图标-->
                            <span>{{ comments.thumb_count }}</span>   <!--点赞数-->
                            <div class="article-brief-text">
                                {{ article.brief }}
                            </div>
                        </div>
                    </div>
                </div>
                <hr>
            {% endfor %}
        </div>
        <div class="wrap-right">
                rightfd
        </div>
        <div class="clear-both"></div>
    {% endblock %}
    View Code

    点赞及评论的显示:

    由于点赞和评论存在同一张表中,需分开统计。自定义标签:

    custom.py

    @register.simple_tag
    def filter_comment(article_obj): #传递一个模型名字作为参数
        query_set = article_obj.comment_set.select_related()  #反向查询:comment关联了article,通过article查询comment,字段名_set.select_related()优化查询性能
        comments = {
            'comment_count':query_set.filter(comment_type=1).count(),#统计type为1的数量
            'thumb_count':query_set.filter(comment_type=2).count(),#统计type为2的数量
        }
        return comments

    单个页面显示文章详细信息: 

    新建页面article_detail.html来展示文章详细信息(包括标题,作者,发布时间,评论数量,图片等)

    配置blog/urls.py:传入文章id

    urlpatterns = [
        url(r'^$',views.index),
        url(r'category/(d+)',views.category),
        url(r'article_detail/(d+)',views.article_detail,name='article_detail'),
    ]

    views.py:

    def article_detail(request,article_id):
        article_obj = models.Article.objects.get(id=article_id)
        return render(request,'blog/article_detail.html',{'article_obj':article_obj,'category_list':category_list})  #category_list导航栏

    article_detail.html页面依然继承base.html,并加载custom自定义的标签,

    article_detail.html:

    {% extends 'blog/base.html' %}
    {% load custom %}
        {% block page-container %}
        <div class="wrap-left">
            <div class="article-title-bg">
                {{ article_obj.title }}
            </div>
            <div class="article-title-brief">
                <span>作者:{{ article_obj.author.name }}</span>
                <span>{{ article_obj.pub_date }}</span>
                {% filter_comment article_obj as comments %}
                <span class="glyphicon glyphicon-comment" aria-hidden="true"></span>
                <span>{{ comments.comment_count }}</span>
            </div>
            <div class="article-content">
                <img class="article-detail-head-img" src="/static/{{ article_obj.head_img | truncate_url }}">
                {{ article_obj.content }}
            </div>
        </div>
        <div class="wrap-right">
                rightfd
        </div>
        <div class="clear-both"></div>
    {% endblock %}

    在custom.css中添加文章页面中标题,图片及文章正文等样式:

    custome.css

    /*文章标题样式*/
    .article-title-bg{
        font-size: 28px;
        margin: 0;
        position: inherit;
        line-height: 1.5;
        color: #333;
    }
    
    
    /*文章内容样式*/
    .article-content{
        font-size: 16px;
        line-height: 30px;
    }
    
    
    /*文章标题下的简介样式*/
    .article-title-brief{
        margin: 0 15px 0 10px;
        color: #999;
    }
    
    
    /*文章中图片样式*/
    .article-detail-head-img{
        width: 100%;
        margin-top: 20px;
    }

    如何避免csrf攻击?

    关于csrf攻击,请见<关于CSRF的攻击>

    1、页面中加入token:

    base.html:

    <body> {% csrf_token %}
    ,,,
    </body>

    这时,浏览器请求该页面后,在<body>标签中会收到如下信息:

    <input type="hidden" name="csrfmiddlewaretoken" value="Ire076FEoI50ntbU9qJHQZe0xEV1Et37">

    该信息即服务端发给浏览器的token,浏览器再次请求服务端时,需将该token一并发送给服务端,以便服务端来验证浏览器身份:

    article_detail.html:

    <script>
        {#获取token#}
        function getCsrf(){
            return $("input[name='csrfmiddlewaretoken']").val();
        }
    
        $(document).ready(function(){
           $('.comment-box button').click(function(){
               var comment_text = $('.comment-box textarea').val();
               if (comment_text.trim().length<5){
                   alert('评论内容不能少于5个字');
               }else {
                   $.post('{% url 'post_comment' %}', {# 提交的url #}
                           {
                               'comment_type':1,
                               article_id:'{{ article_obj.id }}',
                               parent_comment_id:null,
                                'comment':comment_text.trim(),
                               'csrfmiddlewaretoken':getCsrf() {# 提交到服务端 #}
                           },
    
                           function(callback){
                               console.log(callback);
                           }
                   )
               }
           });
        });
    </script>

    关于登陆后方可评论的逻辑:

    1)默认用户在不登陆的情况下不能对文章进行评论,评论框提示'登陆后评论',并将'评论'连接到login_url。

    2)用户登陆成功后需跳转到评论页面。

    如何实现?

    在'登陆'超链接中,拼接url,加入'next',其参数为当前request.path。

    这样跳转到登陆页面时,url中会带有上篇文章的path。

    在login视图函数的'HttpResponseRedirect' 方法加入该path参数,使其实现:若从评论登陆而来,则登陆成功后跳转回评论页面;若直接请求的主页,则跳转至'blog'主页面。

    article_detail.html:

    <div class="comment-box">
                {% if request.user.is_authenticated %} <!--如果已登陆-->
                    <textarea class="comment-box" rows="3"></textarea>  <!--评论文本框,3行的高度-->
                    <button type="button" style="margin-top: 10px" class="btn btn-success pull-right">评论</button>
                {% else %}
                    <div class="jumbotron">
                        <h4 class="text-center"><a class="btn-link" href="{% url 'login' %}?next={{ request.path }}">登陆</a>后评论</h4>
                    </div>
                {% endif %}
            </div>

    views.py中login函数的修改:

    def user_login(request):
        if request.method == 'POST':
            user = authenticate(username = request.POST.get('username'),password = request.POST.get('password'))
            if user is not None:
                login(request,user)
                return HttpResponseRedirect(request.GET.get('next') or '/blog')
            else:
                login_error = 'wrong username or password!'
                return render(request,'blog/login.html',{'login_error':login_error})
        return render(request,'blog/login.html')

    提交评论到后端数据库:

    1、在视图函数add_comment中,初始化一个models.Comment对象,用来存放前端提交过来的数据(按照comment表的格式定义),然后调用.save()方法写入数据库。

    2、成功写入数据库后,向前端返回字符串表示提交评论成功,前端提示用户。

    以上过程,在前端和后端函数交互过程(通过ajax提交(post)或者获取(get))中传递的信息存放在callback()函数中。

    views.py:

    def add_comment(request):
        if request.method=='POST':
            new_comment_obj = models.Comment(
                article_id = request.POST.get('article_id'),
                parent_comment_id = request.POST.get('parent_comment_id') or None,     #父评论可以为空
                comment_type = request.POST.get('comment_type'),
                user_id = request.user.userinfo.id,
                comment = request.POST.get('comment')
            )
            new_comment_obj.save()
    
        return HttpResponse('add comment success')

    前端页面article_detail.html:

    <script>
        {#获取token#}
        function getCsrf(){
            return $("input[name='csrfmiddlewaretoken']").val();
        }
    
        $(document).ready(function(){
           $('.comment-box button').click(function(){
               var comment_text = $('.comment-box textarea').val();
               if (comment_text.trim().length<5){
                   alert('评论内容不能少于5个字');
               }else {
                   $.post('{% url 'post_comment' %}', {# 提交的url #}
                           {
                               'comment_type':1,
                               article_id:'{{ article_obj.id }}',
                               parent_comment_id:null,
                               'comment':comment_text.trim(),
                               'csrfmiddlewaretoken':getCsrf() {# 提交到服务端 #}
                           },
    
                           function(callback){
                               console.log(callback);
                               if (callback == 'add comment success'){      #后端返回的数据
                                   alert('添加评论成功!')
                               }
                           }
                   )
               }
           });
        });
    </script>

    前端页面展示评论的主要逻辑流程:

    1、当页面加载时,在页面底部展示该文章所有的评论信息。

    2、对文章提交评论后,ajax重新从后台查询最新的评论在前端页面展示。

    3、对评论进行评论,点击评论图标时,会将comment-box(包含评论框和提交按钮)复制一份放在被评论的下方,同时原comment-box将被删除。

    4、提交对评论的评论时,前端页面重新获取comment-list,并将原来的comment-box放回原来位置。

    在展示父评论与子评论时,需要拼接字符串,父子评论之间使用margin-left拼接,以显示对应关系。

    设置提交评论和获取评论url:

    urlpatterns = [
        url(r'comment/$',views.add_comment,name='post_comment'),
        url(r'comment_list/(d+)/$',views.get_comments,name='get_comments'),
    ]

     视图函数views.py:

    def add_comment(request):
        if request.method=='POST':
            new_comment_obj = models.Comment(
                article_id = request.POST.get('article_id'),
                parent_comment_id = request.POST.get('parent_comment_id') or None,
                comment_type = request.POST.get('comment_type'),
                user_id = request.user.id,
                comment = request.POST.get('comment')
            )
            new_comment_obj.save()
    
        return HttpResponse('add comment success')
    
    
    #返回给前端之前,首先生成评论树,然后拼接成字符串
    def get_comments(request,article_id):
        article_obj = models.Article.objects.get(id=article_id)
        comment_tree = comment_hander.build_tree(article_obj.comment_set.select_related())#生成树
        tree_html = comment_hander.render_comment_tree(comment_tree)   #拼接字符串
        return HttpResponse(tree_html)

    拼接字符串在后端函数中处理:

    新建comment_hander.py,用于处理评论字符串的拼接:

    #! _*_ coding:utf-8 _*_
    
    '''
    生成评论树
    '''
    
    def add_node(tree_dic,comment):
        if comment.parent_comment is None:   #如果该评论没有父评论,则将自己放在顶层(父评论)位置
            tree_dic[comment]={}
        else: #如果有父评论,则循环该字典,找到父评论,将自己放父评论后面
            for k,v in tree_dic.items():
                if k == comment.parent_comment: #找到父评论
                    tree_dic[comment.parent_comment][comment]={}
                else:
                    add_node(v,comment) #继续递归查找
    
    
    
    def build_tree(comment_set):
        tree_dic = {}
        for comment in comment_set:
            add_node(tree_dic,comment)
        return tree_dic
    
    
    
    #用于递归字典
    #遍历字典,取完第一层递归取第二层,<div>父</div>与<div>子</div>之间使用margin-left连接
    def render_tree_node(tree_dic,margin_val):
        html = ''
        for k,v in tree_dic.items():
            ele = "<div class='comment-node' style='margin-left:%spx'>" % margin_val + k.comment 
                  + "<span style='margin-left:10px'>%s</span>" % k.user.name 
                  + "<span comment-id= '%s'" % k.id + "style='margin-left:10px' class='glyphicon glyphicon-comment add-comment' aria-hidden='true'></span>" 
                  +"</div>"  #子评论
            html += ele
            html += render_tree_node(v,margin_val+10)
        return html
    
    
    #拼接字符串
    def render_comment_tree(tree_dic):
        html=''
        for k,v in tree_dic.items():
            ele = "<div class='root-comment'>" + k.comment 
                  + '<span style="margin-left:10px">'+ k.user.name +'</span>' 
                  +'<span comment-id= "%s"' % k.id + 'style="margin-left:10px" class="glyphicon glyphicon-comment add-comment" aria-hidden="true"></span>'
                  + "</div>"  #只对父评论生效
            html += ele
            html += render_tree_node(v,10)
        return html
  • 相关阅读:
    Python笔记初识
    visio开发者图形分类个人爱好
    亿图图示与visio结合使用
    xmind visio mindmanager edraw比较
    Pycharm安装步骤
    win32com问题
    Win32.com安装
    Pycharm安装步骤
    Phython笔记初识
    跨域问题解决方案
  • 原文地址:https://www.cnblogs.com/ahaii/p/5761628.html
Copyright © 2020-2023  润新知