[django项目] 在新闻详情页发表评论

新闻详情页

I. 功能需求分析

1>功能

  1. 新闻详情
  2. 加载评论功能
  3. 添加评论功能

II. 新闻详情页

1>业务流程分析

业务流程:

  1. 判断前端传递新闻id是否为空,是否为整数,是否存在

2>接口设计

  1. 接口说明:
类目 说明
请求方法 GET
url定义 /news//
参数格式 url路径参数
  1. 参数说明:
参数名 类型 是否必须 描述
news_id 整数 新闻id
  1. 返回结果:

    html页面,直接通过模板渲染的方式实现

3>后端代码

3.1>后端视图

第一种写法

# 在news/views.py中定义如下视图
class NewsDetailView(View):
    """
    新闻详情视图
    url: '/news//
    """
    # 传递news_id的目的就是为了获取新闻详情页
    def get(self, request, news_id):
        # 1. 校验是否存在
        # 2. 获取数据, 通过select_related获取外键字段
        news = 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:
            return render(request, 'news/news_detail.html', context={
                'news': news,
            })
        else:
            # 如果没有对应的news id 就返回Page Not Found
            return HttpResponseNotFound('

Page Not Found

'
)

第二种写法, 等价于上面的方法, 代码量更少且返回的是404页面

news_queryset = News.objects.select_related('tag', 'author').only(
            'title', 'content', 'update_time', 'tag__name',
            'author__username')
        # 若news的id 不匹配着返回404页面
        news = get_object_or_404(news_queryset, is_delete=False, id=news_id)
        return render(request, 'news/news_detail.html', context={'news': news})

3.2>配置路由

# 在news/urls.py中定义如下路由

urlpatterns = [
	...
    path('news//', views.NewDetailView.as_view(), name='news_detail')
]

到这里可以去页面上试一下, 访问一个不存在的news_id, 看是不是会显示Page Not Found

4>前端代码

4.1>html

{% extends 'base/base.html' %}
{% load static %}
{% block title %}文章详情{% endblock %}
{% block link %}
    <link rel="stylesheet" href="{% static 'css/news/news-detail.css'%}">
{% endblock %}
{% block main_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|safe }}
      article>
      <div class="comment-contain">...div>

    div>
    
{% endblock %}

{{ news.content|safe }}中的safe一定要加, 不然浏览器会认为不安全的内容, 加载就会出错

4.2>css

/* 为文章内容添加样式 */
/* 在static/css/news/news-detail.css文件中需要添加如下内容:*/

.content p {
	font-size: 16px;
	line-height: 26px;
	text-align: justify;
	word-wrap: break-word;
	padding: 3px 0
}

III. 加载新闻评论

1>接口设计

新闻详情页,直接渲染新闻评论

2>后端代码

2.1>模型代码

评论虽然可以直接跟随新闻详情页一起加载, 但并不能支持二级评论

我们可以让Comments外键关联它自己, 这种方法也是在评论种用的最多的

# 本项目涉及二级评论,修改Comments模型,添加一个parent字段
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)

修改模型字段后一定要及时迁移

2.2>导入数据

# 导入测试数据tb_comments_20181222.sql
# 一定要保证tb_users中有id为1,2,3的三个用户,不然导入测试数据会报错
mysql -u用户名 -p -D 数据库名< tb_comments_20181222.sql

2.3>视图代码

# 修改news/views.py中的NewsDetailView视图
class NewDetailView(View):
    def get(self, request, news_id):
        news = 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:
            #获取评论
            comments = Comments.objects.select_related('author', 'parent').only(
                'content', 'author__username', 'update_time', 'parent__author__username', 'parent__content',
                'parent__update_time').filter(is_delete=False, news_id=news_id)
            return render(request, 'news/news_detail.html', context={
                'news': news,
                'comments': comments
            })
        else:
            return HttpResponseNotFound('

Page not found

'
)

3>前端代码

3.1>css

/* 在static/css/news/news-detail.css中添加如下代码: */
.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;
    display:inline-block;
}
.parent_comment_text .comment_time{
   display: inline-block;
   float:right;
}

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

.reply_a_tag{
    font-size:12px;
    color:#999;
    text-indent:20px;
    margin:10px 0 0 20px;
    background:url('/static/images/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============ */
将content_icon.png图片放到static/images/中

3.2>html


<div class="comment-contain">
            <div class="comment-pub clearfix">...div>
            <ul class="comment-list">
                {% for comment in comments %}
                    <li class="comment-item">
                        <div class="comment-info clearfix">
                            <img src="/static/images/avatar.jpeg" alt="avatar" class="comment-avatar">
                            <span class="comment-user">{{ comment.author.username }}span>
                            <span class="comment-pub-time">{{ comment.update_time }}span>
                        div>
                        <div class="comment-content">{{ comment.content }}div>

                        {% if comment.parent %}
                            <div class="parent_comment_text">
                                <div class="parent_username">{{ comment.parent.author }}div>
                                <div class="comment_time">{{ comment.parent.update_time }}div>

                                <div class="parent_content_text">
                                    {{ comment.parent.content }}
                                div>

                            div>
                        {% endif %}

                        <a href="javascript:void(0);" class="reply_a_tag right_float">回复a>
                        <form class="reply_form left_float" comment-id="{{ comment.id }}"
                              news-id="{{ 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 comments %}

            ul>
        div>

3.3>js代码

// 在static/js/news/news_detail.js中加入如下代码:

$(function () {
  $('.comment-list').delegate('a,input', 'click', function () {
    //获取回复按钮的class属性
    let sClassValue = $(this).prop('class');
    // 如果点击的是回复按钮,就显示输入框
    if (sClassValue.indexOf('reply_a_tag') >= 0) {
      $(this).next().toggle();
    }
    // 如果点击的是取消按钮,就隐藏输入框
    if (sClassValue.indexOf('reply_cancel') >= 0) {
      $(this).parent().toggle();
    }

    if (sClassValue.indexOf('reply_btn') >= 0) {
      // 评论
    }
  });
 
});

要记得在news_detail.html中引入

{% block script %}
    <script src="{% static 'js/news/news_detail.js' %}">script>
{% endblock %}

IIII. 添加新闻评论功能

1>业务流程分析

业务处理流程:

  1. 判断用户是否登录
  2. 判断前端传的新闻id是否为空,是否为整数,是否存在
  3. 判断评论内容是否为空
  4. 判断是否有父评论,父评论id是否与新闻id匹配
  5. 保存新闻评论

2>接口设计

  1. 接口说明:
类目 说明
请求方法 POST
url定义 /news//comment/
参数格式 url路径参数,表单参数
  1. 参数说明:
参数名 类型 是否必须 描述
news_id 整数 新闻id
content 字符串 新闻评论内容
parent_id 整数 父评论id

注意:post请求需要携带csrftoken

  1. 返回结果:
{
	"errno": "0",
	"errmsg": "",
	"data": {
		"news_id": 1170,
		"content_id": 3569,
		"content": "评论比较中肯。",
		"author": "admin",
		"update_time": "2019年08月19日 16:00",
		"parent": {
			"news_id": 1170,
			"content_id": 893,
			"content": "行文思路简单肤浅,文章结构平面呆板。",
			"author": "xinlan",
			"update_time": "2018年12月21日 11:17",
			"parent": null
		}
	}
}

3>后端代码

3.1>视图代码

# 在news/views.py中编写如下视图
class NewsCommentView(View):
    """
    添加评论视图
    url: /news//comment/
    """
    def post(self, request, news_id):
        # 是否登录
        if not request.user.is_authenticated:
            return json_response(errno=Code.SESSIONERR, errmsg=error_map[Code.SESSIONERR])
        # 新闻是否存在
        if not News.objects.only('id').filter(is_delete=False, id=news_id).exists():
            return json_response(errno=Code.PARAMERR, errmsg='新闻不存在!')

        content = request.POST.get('content')
        # 内容是否为空
        if not content:
            return json_response(errno=Code.PARAMERR, errmsg='评论内容不能为空!')

        # 父id是否正常
        parent_id = request.POST.get('parent_id')
        if parent_id:
            try:
                parent_id = int(parent_id)
                if not Comments.objects.only('id').filter(is_delete=False, id=parent_id, news_id=news_id).exists():
                    return json_response(errno=Code.PARAMERR, errmsg=error_map[Code.PARAMERR])
            except Exception as e:
                logger.info('前端传递过来的parent_id异常\n{}'.format(e))
                return json_response(errno=Code.PARAMERR, errmsg='未知异常')

        # 保存到数据库
        new_comment = Comments()
        new_comment.content = content
        new_comment.news_id = news_id
        new_comment.author = request.user
        new_comment.parent_id = parent_id if parent_id else None
        new_comment.save()

        return json_response(data=new_comment.to_dict_data())

3.2>序列化comment对象

# 在news/models.py的Comment模型中添加如下方法,用来序列化
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.astimezone().strftime('%Y年%m月%d日 %H:%M'),
        'parent': self.parent.to_dict_data() if self.parent else None
    }
    return comment_dict

3.4>路由

# 在news/urls.py中添加如下路由
path('news//comment/', views.NewsCommentView.as_view(), name='news_comment'),

4>前端代码

4.1>html



        <div class="comment-contain">
            <div class="comment-pub clearfix">
                <div class="new-comment">
                    文章评论(<span class="comment-count">0span>)
                div>
                {% if user.is_authenticated %}

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

4.2>js代码

// 修改static/js/news/news_detail.js中的代码如下
// 修改static/js/news/news_detail.js中的代码如下
$(function () {
    // 对评论进行评论
    $('.comment-list').delegate('a,input', 'click', function () {
        //获取回复按钮的class属性
        let sClassValue = $(this).prop('class');
        // 如果点击的是回复按钮,就显示输入框
        if (sClassValue.indexOf('reply_a_tag') >= 0) {
            $(this).next().toggle();
        }
        // 如果点击的是取消按钮,就隐藏输入框
        if (sClassValue.indexOf('reply_cancel') >= 0) {
            $(this).parent().toggle();
        }

        if (sClassValue.indexOf('reply_btn') >= 0) {
            // 评论
            let $this = $(this);
            let news_id = $this.parent().attr('news-id');
            let parent_id = $this.parent().attr('comment-id');
            let content = $this.prev().val();
            if (!content) {
                message.showError('请输入评论内容!');
                return
            }
            $
                .ajax({
                    url: '/news/' + news_id + '/comment/',
                    type: 'POST',
                    data: {
                        content: content,
                        parent_id: parent_id
                    },
                    dataType: "json"
                })

                .done((res) => {
                    if (res.errno === '0') {
                        let comment = res.data;
                        let html_comment = `
  • avatar ${comment.author}
    ${comment.content}
    ${comment.parent.author}
    ${comment.parent.update_time}
    ${comment.parent.content}
    ${comment.update_time}
    回复
    ${comment.content_id}" news-id="${comment.news_id}">
  • `
    ; message.showSuccess('评论成功!'); setTimeout(() => { $('.comment-list').prepend(html_comment); }, 800); $this.prev().val(''); // 清空输入框 $this.parent().hide(); // 关闭评论框 } else if (res.errno === '4101') { // 用户未登录 message.showError(res.errmsg); setTimeout(() => { window.location.href = '/user/login/' }, 800) } else { // 失败 message.showError(res.errmsg) } }) .fail(() => { message.showError('服务器超时,请重试') }) } }); // 对新闻评论 let $newsComment = $('.logged-comment input'); // 新闻评论框 let $sendComment = $('.comment-pub .comment-btn'); // 新闻评论按钮 $sendComment.click(function () { let $this = $(this); if ($this.prev().hasClass('please-login-comment')) { message.showError('未登录,请登录后再评论!'); setTimeout(() => { window.location.href = '/user/login/' }, 800); return } let news_id = $this.prev().attr('news-id'); let content = $newsComment.val(); if (!content) { message.showError('请输入评论内容!'); return } $ .ajax({ url: '/news/' + news_id + '/comment/', type: 'POST', data: { content: content }, dataType: 'json' }) .done((res) => { if (res.errno === '0') { let comment = res.data; let html_comment = `
  • avatar ${comment.author} ${ comment.update_time }
    ${comment.content}
    回复
    ${comment.content_id}" news-id="${comment.news_id}">
  • `
    ; message.showSuccess('评论成功!'); setTimeout(() => { $(".comment-list").prepend(html_comment); }, 800); // 清空 $newsComment.val(''); } else if (res.errno === '4101') { // 用户未登录 message.showError(res.errmsg); setTimeout(() => { window.location.href = '/user/login/' }, 800) } else { message.showError(res.errmsg); } }) .fail(() => { message.showError('服务器超时,请重试!'); }) }) });

    你可能感兴趣的:(django笔记)