11评论功能

分析

  • 业务处理流程:
    • 判断前端传的新闻id是否为空,是否为整数,是否不存在
    • 请求方法:GET
    • url定义:/news/<int:news_id>/
    • 请求参数:url路径参数
参数 类型 前端是否必须传 描述
news_id 整数 新闻id
  • 新闻详情页直接通过模板渲染的方式来实现,在新闻详情页直接渲染此新闻的评论信息

后端代码的实现

apps/new/models.py

  • Comments中新增一个字段parent,然后迁移
from django.db import models

from utils.models import ModelBase  #自己定义的基类


class Tag(ModelBase):
    """
    分类/标签
    """
    name = models.CharField(max_length=64,verbose_name='标签名',help_text='标签名')

    class Meta:
        #需要注意的是:不论你使用了多少个字段排序, admin 只使用第一个字段
        ordering = ['-update_time','-id']   #按照指定的字段进行数据库的排序, 用更新时间和id,默认从大到小,前面加负号是从小到大
        db_table = 'tb_tag' #指定该类的数据库表单名字
        verbose_name = '新闻标签'   #在admin站点中显示的名称,给你的模型类起一个更可读的名字
        verbose_name_plural = verbose_name  #显示的复数名称,若未提供该选项, Django 会使用 verbose_name + "s"

    def __str__(self):
        return self.name


class News(ModelBase):
    """
    文章
    """
    title = models.CharField(max_length=150,verbose_name='标题',help_text='标题')   #字符串,显示名字为标题
    digest = models.CharField(max_length=150,verbose_name='摘要',help_text='摘要')   #显示的名字和描述都是‘摘要’
    content = models.TextField(verbose_name='内容',help_text='内容')    #文本
    clicks = models.IntegerField(default=0,verbose_name='点击量',help_text='点击量')  #整形
    image_url = models.URLField(default='',verbose_name='图片url',help_text='图片url')  #继承CharField
    # 一个标签对应多个文章/新闻,一对多,在多(新闻)里外键关联(ForeignKey)一(标签)
    # 外键关联的Tag这里最好用引号,这样不用考虑先后,如果不加引号,Tag模型类必须在News前面
    tag= models.ForeignKey('Tag',on_delete=models.SET_NULL,null=True)
    # 一个作者/用户对应多个文章/新闻,一对多,在多(新闻)里外键关联(ForeignKey)一(作者)
    #on_delete表示外键关联到用户表,SET_NULL表示当用户表删除了该用户,新闻表中不删除,仅仅是把外键置空
    author = models.ForeignKey('users.Users',on_delete=models.SET_NULL,null=True)

    class Meta:
        ordering = ['-update_time','-id']
        db_table = 'tb_news'
        verbose_name = '新闻'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.title


class Comments(ModelBase):
    """
    评论
    回复评论的评论也是评论,和评论公用一个模型类
    评论没有parent字段,回复评论有parent字段
    多次用到时间,要序列化,所以直接在模型进行序列化,不用每个视图再序列化
    """
    content = models.TextField(verbose_name='内容',help_text='内容')
    author = models.ForeignKey('users.Users',on_delete=models.SET_NULL,null=True)
    #CASCADE级联删除,新闻表删除时评论表也一起删除
    news = models.ForeignKey('News',on_delete=models.CASCADE)
    #null针对数据库,字段里的内容可以为空,blank针对表单,字段可以不填 self针对实例对象,自关联字段
    parent = models.ForeignKey('self',on_delete=models.CASCADE,null=True,blank=True)


    def to_dict_data(self):
        comment_dict = {
            'news_id':self.news_id,
            'content_id':self.id,
            'content':self.content,
            'author':self.author.username,
            'update_time':self.update_time.strftime('%Y-%m-%d %H:%M:%S'),
            'parent':self.parent.to_dict_data() if self.parent else None   #parent是Comments的实例对象,用类的to_dict_data方法
        }
        return comment_dict


    class Meta:
        ordering = ['-update_time','-id']
        db_table = 'tb_comments'
        verbose_name = '评论'
        verbose_name_plural = verbose_name

    def __str__(self):
        return f'评论{self.id}'


class HotNews(ModelBase):
    """
    热门文章
    """
    PRI_CHOICES = [ #对优先级参数有所限制
        (1,'第一级'),
        (2,'第二级'),
        (3,'第三级'),
    ]
    news = models.OneToOneField('News',on_delete=models.CASCADE)
    #choices使优先级只能是1,2或者3,default默认为3
    priority = models.IntegerField(choices=PRI_CHOICES,default=3,verbose_name='优先级',help_text='优先级')


    class Meta:
        ordering = ['-update_time','-id']
        db_table = 'tb_hotnews'
        verbose_name = '热门文章'
        verbose_name_plural = verbose_name


    def __str__(self):
        return f'热门新闻{self.id}'


class Banner(ModelBase):
    """
    轮播图
    """
    PRI_CHOICES = [
        (1,'第一级'),
        (2,'第二级'),
        (3,'第三级'),
        (4,'第四级'),
        (5,'第五级'),
        (6,'第六级'),
    ]
    image_url = models.URLField(verbose_name='轮播图url',help_text='轮播图url')
    priority = models.IntegerField(choices=PRI_CHOICES,default=6,verbose_name='优先级',help_text='优先级')
    news = models.OneToOneField('News',on_delete=models.CASCADE)


    class Meta:
        ordering = ['-update_time','-id']
        db_table = 'tb_banner'
        verbose_name = '轮播图'
        verbose_name_plural = verbose_name

    def __str__(self):
        return f'轮播图{self.id}'
  • 导入测试数据
    mysql -uroot -p -D web_prv < tb_comments_20181222.sql

apps/news/views.py

  • 在NewsDetailView中添加序列化
import logging

from django.shortcuts import render
from django.views import View
from django.core.paginator import Paginator,EmptyPage   #不存在的页码报错
from django.http import Http404

from apps.news import models
from apps.news import constants

from utils.json_fun import to_json_data
from utils.res_code import Code,error_map
logger = logging.getLogger('django')

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


class IndexView(View):
    """
    constants
    """
    def get(self,request):
        # tags = models.Tag.objects.filter(is_delete=False)
        #通过表中的is_delete判断是否被删除
        tags = models.Tag.objects.only('id','name').filter(is_delete=False) #only确定要查询的字段,其他的不查
        #使用select_related关联表优化,一对一和一对多都可用,查询集,用切片获取对象
        hot_news = models.HotNews.objects.select_related('news').only('news__title','news__image_url',\
          'news_id').filter(is_delete=False).order_by('priority','-news__clicks')[0:constants.SHOW_HOTNEWS_COUNT]

        # context = {
        #     'tags':tags,
        # }
        #用模板上下文     将当前方法下的变量都传进locals,python内置函数
        # return render(request,'news/index.html',context=context)
        return render(request,'news/index.html',locals())

#ajax请求
#传参 tag_id  page
#后台的返回  图片,标题,摘要,作者,标签,更新时间,文章id
#请求方式:GET
#/news/
#url查询字符串   ?tag_id=1&page=2    /news/?tag_id=1&page=2


class NewsListView(View):
    """
    对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用select_related 来对QuerySet进行优化。
    select_related 返回一个QuerySet,当执行它的查询时它沿着外键关系查询关联的对象的数据。
    它会生成一个复杂的查询并引起性能的损耗,但是在以后使用外键关系时将不需要数据库查询
    """

    def get(self,request):
        """
        # 1.获取参数
        # 2.校验参数
        # 3.从数据库拿数据
        # 4.分页
        # 5.序列化输出
        #6.返回给前端
        :param request:
        :return:
        """
        #1.获取参数,校验参数
        #如果通过不正当手段导致int转换不成功时,返回最新资讯给前端
        try:
            # 如果没传或者格式不对得不到数据,为0,则把最新资讯id设置为0,当刚进入首页没进行选择标签时起作用,即默认为最新资讯
            tag_id = int(request.GET.get('tag_id',0))   #get到的是str类型,存入数据库是int类型
        except Exception as e:
            logger.error('标签错误:\n{}'.format(e))
            tag_id = 0
        try:
            page = int(request.GET.get('page',1))
        except Exception as e:
            logger.error('页码错误:\n{}'.format(e))
            page = 1
        #3.从数据库中拿数据
        #title,digest(摘要),image_url,update_time
        #select_related用于多表查询,是优化措施,参数是关联外键的字段,id为主键,无论怎样都会查到,所以不需要列出来
        #News中的tag字段对应Tag标签中的name字段,News中的author字段对应Users中的username字段
        news_queryset = models.News.objects.select_related('tag','author').only('title','digest',\
                        'image_url','update_time','tag__name','author__username')
        #is_delete:是否被逻辑删除,tag_id=0在queryset中表示为空
        news = news_queryset.filter(is_delete=False,tag_id=tag_id) or news_queryset.filter(is_delete=False)
        #4.分页
        paginator = Paginator(news,constants.PER_PAGE_NEWS_COUNT)   #第一个参数是数据,第二个参数是每页的新闻个数
        try:
            news_info = paginator.page(page)    #对应页的新闻
        except EmptyPage:
            logger.error('用户访问页数大于总页数')
            news_info = paginator.page(paginator.num_pages) #展示最大页码的新闻
        #5.序列化输出
        news_info_list = []
        for n in news_info:
            news_info_list.append({
                'id':n.id,
                'title':n.title,
                'digest':n.digest,
                'image_url':n.image_url,
                'update_time':n.update_time.strftime('%Y-%m-%d %H:%M:%S'),
                'tag_name':n.tag.name,  #tag的name属性
                'author':n.author.username,
            })
        data = {    #返回给前端的,名一定要和前端对应否则没作用,细心
            'news':news_info_list,
            'total_pages':paginator.num_pages    #总页码数
        }
        #6.返回给前端
        return to_json_data(data=data)


class NewsBanner(View):
    """
    ajax
    """
    #不传参,不改数据,用get
    def get(self,request):
        banners = models.Banner.objects.select_related('news').only('image_url','news_id','news__title').\
            filter(is_delete=False).order_by('priority')[0:constants.SHOW_BANNER_COUNT]
        #序列化输出
        banners_info_list = []
        for b in banners:
            banners_info_list.append(
                {
                    'image_url':b.image_url,
                    'news_id':b.news_id,
                    'news_title':b.news.title
                }
            )
        data = {
            'banners':banners_info_list,
        }
        return to_json_data(data=data)  #返回给前端

#文章详情
#参数:news_id
#title author__username update_time tag_names content

class NewsDetailView(View):
    """
    /news/<int:news_id>/
    将文章id携带到url
    """
    def get(self,request,news_id):
        news = models.News.objects.select_related('tag','author').only('title','content','update_time',\
                   'tag__name','author__username').filter(is_delete=False,id=news_id).first()
        if news:
            """
            字段:content,update_time,parent.username,parent.content,parent.update_time
            """
            comments = models.Comments.objects.select_related('author','parent').only('content','update_time',\
                           'author__username','parent__content','parent__author__username','parent__update_time')\
                            .filter(is_delete=False,news_id=news_id)
            #序列化输出
            comments_list = []
            for comm in comments:
                comments_list.append(comm.to_dict_data())
            return render(request,'news/news_detail.html',locals())
        else:
            raise Http404('新闻id不存在'.format(news_id))

前端代码实现

new_detail.html

{% extends 'base/base.html' %}
{% load static %}
{% block meta %}
<meta name="renderer" content="webkit">
  <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
{% endblock %}
  {% block title %}<title>news-detail</title>{% endblock %}
  {% block link %}<link rel="stylesheet" href="{% static 'css/news/news-detail.css' %}">{% endblock %}
  {% block contain %}
    <div class="news-contain">
      <h1 class="news-title">{{ news.title }}</h1>
      <div class="news-info">
        <div class="news-info-left">
          <span class="news-author">{{ news.author.username }}</span>
          <span class="news-pub-time">{{ news.update_time }}</span>
          <span class="news-type">{{ news.tag.name }}</span>
        </div>
      </div>
      <article class="news-content">
{#        {{ news.content}}#}
        {{ news.content|safe }}
      </article>
    <div class="comment-contain">
      <div class="comment-pub clearfix">
        <div class="new-comment">
          文章评论(<span class="comment-count">0</span>)
        </div>

        {% if user.is_authenticated %}
          <div class="comment-control logged-comment" news-id="{{ news.id }}">
            <input type="text" placeholder="请填写评论">
            <button class="comment-btn">发表评论</button>
          </div>
        {% else %}
          <div class="comment-control please-login-comment" news-id="{{ news.id }}">
            <input type="text" placeholder="请登录后参加评论" readonly>
            <button class="comment-btn">发表评论</button>
          </div>
        {% endif %}

      </div>

      <ul class="comment-list">
        {% for one_comment in comments_list %}
          <li class="comment-item">
            <div class="comment-info clearfix">
              <img src="{% static 'img/avatar.jpeg' %}" alt="avatar" class="comment-avatar">
              <span class="comment-user">{{ one_comment.author }}</span>
            </div>
            <div class="comment-content">{{ one_comment.content }}</div>

            {% if one_comment.parent %}
              <div class="parent_comment_text">
                <div class="parent_username">{{ one_comment.parent.author }}</div>
                <br/>
                <div class="parent_content_text">
                  {{ one_comment.parent.content }}
                </div>
              </div>
            {% endif %}

            <div class="comment_time left_float">{{ one_comment.update_time }}</div>
            <a href="javascript:;" class="reply_a_tag right_float">回复</a>
            <form class="reply_form left_float" comment-id="{{ one_comment.content_id }}"
                  news-id="{{ one_comment.news_id }}">
              <textarea class="reply_input"></textarea>
              <input type="button" value="回复" class="reply_btn right_float">
              <input type="reset" name="" value="取消" class="reply_cancel right_float">
            </form>

          </li>
        {% endfor %}

      </ul>
    </div>

    </div>
    <!-- side end -->
  {% endblock %}
{% block domready %}
{#  要导入message.js,否则news_detail.js的message会报错Uncaught ReferenceError: message is not defined#}
  <script src="{% static 'js/base/message.js' %}"></script>
  <script src="{% static 'js/news/news_detail.js' %}"></script>
{% endblock %}

news_detail.css

/* ================= main start ================= */
#main {
    margin-top: 25px;
    min-height: 700px;
}
/* ========= news-contain start ============ */
#main .news-contain {
    width: 800px;
    background: #fff;
    float: left;
    padding: 0 25px;
    box-sizing: border-box;
}
.news-contain .news-title {
  margin-top: 20px;
  font-size: 30px;
  line-height: 60px;
}
.main-contain .news-info .news-info-left{
  line-height: 40px;
}
.news-info .news-info-left span {
  margin:0 5px;
  color: #878787;
}
.news-info-left span.news-author {
  margin-left: 0;
}
.news-info-left span.news-type{
  background: #01018c;
  color: #fff;
  padding: 0 20px;
  border-radius: 5px;
}
.news-contain .news-content {
  margin-top: 25px;
  line-height: 30px;
}
.main-contain .news-content  img {
  width: 100% !important;
}

#main .comment-contain {
  width: 750px;
  border-top: 1px solid #ddd;
   padding-top: 20px;
  margin-top: 20px;
}
.comment-contain .comment-pub {
  width: 100%;
}
.comment-pub .comment-control {
  width: 100%;
  margin-top: 20px;
}
.comment-pub .comment-control input {
  width: 100%;
  height: 40px;
  padding-left: 1.3em;
  border-radius: 5px;
  border: 1px solid #ddd;
  box-sizing: border-box;
}
.comment-pub .comment-btn {
  float: right;
  border: none;
  background: #8117fd;
  color: #fff;
  width: 80px;
  line-height: 20px;
  margin-top: 20px;
  border-radius: 5px;
  cursor: pointer;
}
.comment-contain .comment-list {
  width: 100%;
  margin-top: 30px;
}
.comment-list .comment-item {
  border-bottom: 1px solid #ddd;
  margin-bottom: 30px;
}
.comment-item .comment-info {
  color: #878787;
  line-height: 70px;
}
.comment-info .comment-avatar {
  width: 40px;
  height: 40px;
  vertical-align: -12px;
}
.comment-info .comment-user {
   margin-left: 20px;
}

.comment-info .comment-pub-time {
  float: right;

}
.comment-item .comment-content {
  padding: 5px 66px 20px;
}
/* ========= news-contain end ============ */
/* ================= main end ================= */

.comment-list .comment-item {
  /*把这条样式注释掉*/
  /*border-bottom: 1px solid #ddd;*/
  margin-bottom: 30px;
}

/* ========= 为父评论添加样式 start============ */
.left_float{
	float:left;
}

.right_float{
	float:right;
}

.parent_comment_text{
    width:698px;
    padding:8px;
    background: #f4facf;
    margin:10px 0 0 60px;
}

.comment_time{
    font-size:12px;
    color:#999;
    margin:10px 0 0 60px;
}

.parent_comment_text .parent_username{
    font-size:12px;
    color:#000;
    margin-bottom:5px;
}

.parent_comment_text .parent_content_text{
    color:#666;
    font-size:14px;
}

.reply_a_tag{
    font-size:12px;
    color:#999;
    text-indent:20px;
    margin:10px 0 0 20px;
    background:url('/static/img/content_icon.png') left center no-repeat;
}

.reply_form{
    width:718px;
    overflow:hidden;
    margin:10px 0 0 60px;
    display:none;
}

.reply_input{
    float:left;
    width:692px;
    height:30px;
    border-radius:4px;
    padding:10px;
    outline:none;
    border:1px solid #2185ed;
}

.reply_btn,.reply_cancel{
    width:40px;
    height:23px;
    background:#76b6f4;
    border:0px;
    border-radius:2px;
    color:#fff;
    margin:10px 5px 0 10px;
    cursor:pointer;
}

.reply_cancel{
    background:#fff;
    color: #909090;
}
/* ========= 为父评论添加样式 end============ */

猜你喜欢

转载自blog.csdn.net/xiaogeldx/article/details/92023926