分析
- 业务处理流程:
- 判断前端传的新闻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
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============ */