Django项目: 5.新闻主页
一、功能需求分析
-
轮播图
-
推荐文章列表
-
文章标签导航
-
文章列表
-
分页
二、模型设计
根据功能分析,我们需要如下表
1.表和字段分析
-
文章分类表
-
文章表
-
文章评论表
-
推荐文章表
-
轮播图表
2.模型定义
定义一个基类模型,抽取公共字段
# 在utils目录下,创建一个models.py文件,在其中定义一个基类模型 from django.db import models class BaseModel(models.Model): """ 基类,公共字段 """ create_time = models.DateTimeField('创建时间', auto_now_add=True) update_time = models.DateTimeField('更新时间', auto_now=True) is_delete = models.BooleanField('逻辑删除', default=False) class Meta: # 抽象类,用于继承,迁移时不会创建 abstract = True
定义其他模型
# 在news目录下的models.py文件中定义如下数据模型 from django.db import models from utils.models import BaseModel class Tag(BaseModel): """ 文章分类标签模型 """ name = models.CharField('标签名', max_length=64, help_text='标签名') class Meta: ordering = ['-update_time', '-id'] # 排序 db_table = "tb_tag" # 指明数据库表名 verbose_name = "文章标签" # 在admin站点中显示的名称 verbose_name_plural = verbose_name # 显示的复数名称 def __str__(self): return self.name class News(BaseModel): """ 文章模型 """ title = models.CharField('标题', max_length=150, help_text='标题') digest = models.CharField('摘要', max_length=200, help_text='摘要') content = models.TextField('内容', help_text='内容') clicks = models.IntegerField('点击量', default=0, help_text='点击量') image_url = models.URLField('图片url', default='', help_text='图片url') tag = models.ForeignKey('Tag', on_delete=models.SET_NULL, null=True) author = models.ForeignKey('user.User', on_delete=models.SET_NULL, null=True) class Meta: ordering = ['-update_time', '-id'] # 排序 db_table = "tb_news" # 指明数据库表名 verbose_name = "新闻" # 在admin站点中显示的名称 verbose_name_plural = verbose_name # 显示的复数名称 def __str__(self): return self.title class Comments(BaseModel): """ 评论模型 """ content = models.TextField('内容', help_text='内容') author = models.ForeignKey('user.User', on_delete=models.SET_NULL, null=True) news = models.ForeignKey('News', on_delete=models.CASCADE) class Meta: ordering = ['-update_time', '-id'] # 排序 db_table = "tb_comments" # 指明数据库表名 verbose_name = "评论" # 在admin站点中显示的名称 verbose_name_plural = verbose_name # 显示的复数名称 def __str__(self): return '<评论{}>'.format(self.id) class HotNews(BaseModel): """ 推荐文章表 """ news = models.OneToOneField('News', on_delete=models.CASCADE) priority = models.IntegerField('优先级', help_text='优先级') class Meta: ordering = ['-update_time', '-id'] # 排序 db_table = "tb_hotnews" # 指明数据库表名 verbose_name = "热门新闻" # 在admin站点中显示的名称 verbose_name_plural = verbose_name # 显示的复数名称 def __str__(self): return '<热门新闻{}>'.format(self.id) class Banner(BaseModel): """ 轮播图 """ image_url = models.URLField('轮播图url', help_text='轮播图url') priority = models.IntegerField('优先级', help_text='优先级') news = models.OneToOneField('News', on_delete=models.CASCADE) class Meta: ordering = ['priority', '-update_time', '-id'] # 排序 db_table = "tb_banner" # 指明数据库表名 verbose_name = "轮播图" # 在admin站点中显示的名称 verbose_name_plural = verbose_name # 显示的复数名称 def __str__(self): return '<轮播图{}>'.format(self.id)
三、文章标签导航功能
1.接口设计
-
接口说明:
类目 说明 请求方法 GET url定义 /
参数格式 无参数 -
返回结果
返回新闻页面,直接在模板渲染
2.后端代码
# 在news/views.py文件中定义如下视图 from django.shortcuts import render from .models import Tag def index(request): """ 新闻首页视图 :param request: :return: """ tags = Tag.objects.only('id', 'name').filter(is_delete=False) return render(request, 'news/index.html', context={ 'tags': tags })
导入tag测试数据,或者直接用Navicat软件在tb_tag表中添加数据(数据见下方代码中引号里的内容,共6个),,,因为前段设置的原因,必须要跟我的内容一样,后面会用到
# insert news tag data INSERT INTO tb_tag(name, create_time, update_time, is_delete) values ('Python基础', now(), now(), 0), ('Python高级', now(), now(), 0), ('Python函数', now(), now(), 0), ('PythonGUI', now(), now(), 0), ('Linux教程', now(), now(), 0), ('Python框架', now(), now(), 0);
# 修改templates/news/index.html中 news-nav部分代码如: <!-- news-nav start--> <nav class="news-nav"> <ul class="clearfix"> <li class="active"><a href="javascript:void(0)">最新资讯</a></li> {% for tag in tags %} <li><a href="javascript:void(0)" data-id="{{ tag.id }}">{{ tag.name }}</a> </li> {% endfor %} </ul> </nav> <!-- news-nav end -->
1.业务流程分析
-
判断前端传递标签分类id是否为空,是否为整数,是否超过范围
-
判断前端传递当前文章页数是否为空,是否为整数,是否超过范围
2.接口设计
接口说明:
类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | /news/ |
参数格式 | 查询参数 |
参数说明:
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
tag | 整数 | 是 | 标签分类id |
page | 整数 | 是 | 当前文章页数 |
返回结果:
{ "errno": "0", "errmsg": "", "data": { "total_pages": 61, "news": [ { 'id': 'xxx', "digest": "在python用import或者from...import或者from...import...as...来导入相应的模块,作用和使用方法与C语言的include头文件类似。其实就是引入...", "title": "import方法引入模块详解", "author": "python", "image_url": "/media/jichujiaochen.jpeg", "tag_name": "Python基础", "update_time": "2018年12月17日 14:48" }, { 'id': 'xxx' "digest": "如果你原来是一个php程序员,你对于php函数非常了解(PS:站长原来就是一个php程序员),但是现在由于工作或者其他原因要学习python,但是p...", "title": "给曾经是phper的程序员推荐个学习网站", "author": "python", "image_url": "/media/jichujiaochen.jpeg", "tag_name": "Python基础", "update_time": "2018年12月17日 14:48" } ] } }
3.后端代码
在项目根目录下创建一个media文件夹,用于存放新闻图片以及用户上传文件。
# 在settings.py文件中添加 # 媒体文件配置 MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
django在调试模式下提供静态文件服务,为了能够返回media中的媒体文件还需在根urls.py中做如下配置
# 在根urls.py中加上static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('', include('news.urls')), path('', include('verification.urls')), path('user/', include('user.urls')) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
导入测试数据,为了测试数据的导入,请确保表名一致
# 在xshell中导入测试数据,在xshell中通过rz命令,将tb_news_20181217.sql文件上传到虚拟机
mysql -u 用户名 -p -D 数据库名 < tb_news_20181217.sql
文件我已经放在我的文件栏里(名字是sql数据包.rar,解压后有4个文件,分别是标签表,新闻表,热门新闻表和轮播图表)
视图代码
import logging from django.shortcuts import render from django.views import View from django.core.paginator import Paginator from django.db.models import F from .models import Tag, News from . import constants from utils.json_res import json_response logger = logging.getLogger('django') def index(request): """ 新闻首页视图 url: / :param request: :return: """ tags = Tag.objects.only('id', 'name').filter(is_delete=False) return render(request, 'news/index.html', context={ 'tags': tags }) class NewsListView(View): """ 新闻列表视图 url: /news/ args: tag, page """ def get(self, request): # 1.获取参数 try: tag_id = int(request.GET.get('tag', 0)) except Exception as e: logger.error('标签错误:\n{}'.format(e)) tag_id = 0 try: page = int(request.GET.get('page', 0)) except Exception as e: logger.error('页码错误:\n{}'.format(e)) page = 1 # 使用only返回的是对象,所以传递到前端时需要迭代处理 # news_queryset = News.objects.select_related('tag', 'author').only( # 'title', 'digest', 'image_url', 'update_time', 'tag__name', 'author__username') # 2.获取查询集 (values返回的是字典,only返回的是对象) ############## 重要 start ################# # queryset:惰性 不会去数据库 # 构造,切片,过滤,传递的时候通常查询集不真正的去数据库查询 # 什么时候,去数据库查呢: # 1.迭代 # 2.切片,只要不是跳着去查就不会到数据库中查询,只有中间隔着数据切片的时候才会去数据库中查询,切出一个元素的时候 # 3.照顾会在控制台打印的时候 # 4.序列化缓存,把查询集的内容存到redis,内存 # 5.使用len()方法获取长度,count # 6.使用list()方法转换类型 # 7.bool,去判断是否为空的时候 # QuerySet的缓存 # 什么时候不缓存 # 只执行查询集的一部分 # 简单的打印不换车:Q = News.objects.all() print(Q[1]) # 不会被缓存 ############## 重要 end ################# news_queryset = News.objects.values('id', 'title', 'digest', 'image_url', 'update_time').annotate( tag_name=F('tag__name'), author=F('author__username')) # 3.过滤 # if tag_id: # news = news_queryset.fileter(is_delete=False, tag_id=tag_id) # else: # news = news_queryset.fileter(is_delete=False) 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) # 见下文讲解 # 获取当前页数据 get_page 可以容错 news_info = paginator.get_page(page) # 5.返回数据 data = { 'total_pages': paginator.num_pages, # 分了多少页 'news':list(news_info) } return json_response(data=data)
paginator(分页器)方法:
Paginator.get_page(number)此方法为2.0新方法。
# 2.分页解析 paginator = Paginator(docs, 5) # 每5份内容分页一次 # 2.2 拿到前段发送过来的page这个查询参数的值 page = paginator.get_page(request.GET.get('page',1)) # request.GET.get('page',1) 获取url参数,127.0.0.1:8000/?page=<value> # 非法数值则返回1 数值为空也返回1 如 127.0.0.1:8000/?page=asdsa # 获取当前(页码)所需要的文章列表 相当于一个容器
因为json默认不支持datetime类型数据,所以自定义json编码器(既让新闻的时间序列化)
# 在utils/json_res.py文件中添加自定义的json编码器以便能够序列化datetime数据类型 import json import datetime from django.http import JsonResponse from .res_code import Code # json编码器 # 自定义序列化器1,处理时间字段(老一点的版本这个就会报错) #class MyJSONEncoder(json.JSONEncoder): # def default(self, o): # if isinstance(o, datetime.datetime): # return o.astimezone().strftime('%Y-%m-%d %H:%M:%S') # 转换为本地时间 # 自定义序列化器2,处理时间字段(2.1.10django版本 DjangoJSONEncoder能够帮我们自动的实现) class MyJSONEncoder(DjangoJSONEncoder): def default(self, o): if isinstance(o, datetime.datetime): return o.astimezone().strftime('%Y-%m-%d %H:%M:%S') # 转换为本地时间 def json_response(errno=Code.OK, errmsg='', data=None, kwargs=None): json_dict = { 'errno': errno, 'errmsg': errmsg, 'data': data } if kwargs and isinstance(kwargs, dict) : json_dict.update(kwargs) return JsonResponse(json_dict, encoder=MyJSONEncoder)
定义常量
# 在news目录下constants.py中定义如下常量: # 每页新闻数 PER_PAGE_NEWS_COUNT = 5
路由配置
from django.urls import path from . import views # url的命名空间 app_name = 'news' urlpatterns = [ path('', views.index, name='index'), # 将这条路由命名为index path('news/', views.NewsListView.as_view(), name='news_list') ]
4.前端代码
前端html页面代码修改
<!-- news-contain start 清空 ul中的内容 --> <div class="news-contain"> <ul class="news-list"> </ul> </div> <!-- news-contain end -->
js代码
// static/js/news/index.js $(function () { // 新闻列表 let $newNavLi = $('.news-nav ul li'); // 标签li let iPage = 1; // 默认第一页 let iTotalPage = 1; // 默认总页数为1 let iCurrentTagId = 0; // 默认分类标签为0 let bIsLoadData = true; // 是否正在向后台加载数据 fn_load_content(); // 点击分类标签 $newNavLi.click(function () { // 点击分类标签,则为点击的标签加上一个active的class属性 // 并移除其他兄弟元素上的active的class属性 $(this).addClass('active').siblings('li').removeClass('active'); // 获取绑定在data-id属性上的tag_id let iClickTagId = $(this).children('a').attr('data-id'); if (iClickTagId !== iCurrentTagId){ iCurrentTagId = iClickTagId; // 记录当前分类id // 重置分页参数 iPage = 1; iTotalPage = 1; fn_load_content() } }); // 页面滚动加载 $(window).scroll(function () { // 浏览器窗口高度 let showHeigtht = $(window).height(); // 整个网页高度 let pageHeight = $(document).height(); //页面可以滚动的距离 let canScrollHeight = pageHeight - showHeigtht; // 页面滚动了多少, 整个是随着页面滚动实时变化的 let nowScroll = $(document).scrollTop(); if ((canScrollHeight - nowScroll) < 100){ if(!bIsLoadData){ bIsLoadData = true; //判断页数,去更新新闻,小于总数才加载 if(iPage < iTotalPage){ iPage += 1; fn_load_content(); }else { message.showInfo('已全部加载,没有更多内容!'); $('a.btn-more').html('已全部加载,没有更多内容!') } } } }); // 向后端获取新闻列表数据 function fn_load_content() { $.ajax({ url: '/news/', type: 'GET', data:{ tag: iCurrentTagId, page: iPage }, dataType: 'json', success: function (res) { if(res.errno === '0'){ iTotalPage = res.data.total_pages; if(iPage === 1){ // 第一页清空内容 $('.news-list').html('') } res.data.news.forEach(function (one_news) { let content = ` <li class="news-item"> <a href="https://www.shiguangkey.com/course/2432" class="news-thumbnail" target="_blank"> <img src="${one_news.image_url}" alt="${one_news.title}" title="${one_news.title}"> </a> <div class="news-content"> <h4 class="news-title"><a href="#">${one_news.title}</a> </h4> <p class="news-details">${one_news.digest}</p> <div class="news-other"> <span class="news-type">${one_news.tag_name}</span> <span class="news-time">${one_news.update_time}</span> <span class="news-author">${one_news.author}</span> </div> </div> </li>`; $('.news-list').append(content); }); // $('.news-list').append($('<a href="javascript:void(0);" class="btn-more">滚动加载更多</a>')); //数据加载完毕,设置正在加载数据变量为false,表示当前没有加载数据 bIsLoadData = false; $('a.btn-more').html('滚动加载更多') }else { // 加载失败,打印错误信息 message.showError(res.errmsg) } }, error: function () { message.showError('服务器超时,请重试!') } }); } });
五、轮播图功能
1. 接口设计
-
接口说明:
类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | /news/banners/ |
参数格式 | 无参数 |
返回结果:
{ "errno": "0", "errmsg": "OK", "data": { "banners": [ { 'image_url': '/media/jichujiaochen.jpeg', 'news_id': 221, 'news_title': "python 算法快速排序" }, { "image_url": "/media/python_advanced.jpg", "news_id": 707, "news_title": "Python 序列与映射的解包操作" } ] } }
2.后端代码
视图代码
# 在news目录下views.py中创建如下视图 class NewsBannerView(View): """ 轮播图视图 url:/news/banners/ """ def get(self, request): banners = Banner.objects.values('image_url', 'news_id').annotate( news_title=F('news__title') ).filter(is_delete=False)[:constants.SHOW_BANNER_COUNT] data = { 'banners': list(banners) } return json_response(data=data)
定义常量
# 在news目录下constants.py中定义如下常量 # banner页展示数量 SHOW_BANNER_COUNT = 6
路由
# news目录下urls.py中定义如下路由: from django.urls import path from . import views # url的命名空间 app_name = 'news' urlpatterns = [ path('', views.index, name='index'), # 将这条路由命名为index path('news/', views.NewsListView.as_view(), name='news_list'), path('news/banners/', views.NewsBannerView.as_view(), name='news_banner') ]
3.前端代码
html代码
<!-- 修改templates/news/index.html中banner部分的代码如下 --> <!-- banner start --> <div class="banner"> <ul class="pic"> <!--淡入淡出banner--> </ul> <a href="javascript:void(0);" class="btn prev"> <i class="PyWhich py-arrow-left"></i></a> <a href="javascript:void(0);" class="btn next"> <i class="PyWhich py-arrow-right"></i></a> <ul class="tab"> <!-- 按钮数量必须和图片一致 --> </ul> </div> <!-- banner end -->
js代码
<!-- static/js/news/index.js --> $(function () { // 新闻列表 let $newNavLi = $('.news-nav ul li'); // 标签li let iPage = 1; // 默认第一页 let iTotalPage = 1; // 默认总页数为1 let iCurrentTagId = 0; // 默认分类标签为0 let bIsLoadData = true; // 是否正在向后台加载数据 fn_load_content(); // 点击分类标签 $newNavLi.click(function () { // 点击分类标签,则为点击的标签加上一个active的class属性 // 并移除其他兄弟元素上的active的class属性 $(this).addClass('active').siblings('li').removeClass('active'); // 获取绑定在data-id属性上的tag_id let iClickTagId = $(this).children('a').attr('data-id'); if (iClickTagId !== iCurrentTagId){ iCurrentTagId = iClickTagId; // 记录当前分类id // 重置分页参数 iPage = 1; iTotalPage = 1; fn_load_content() } }); // 页面滚动加载 $(window).scroll(function () { // 浏览器窗口高度 let showHeigtht = $(window).height(); // 整个网页高度 let pageHeight = $(document).height(); //页面可以滚动的距离 let canScrollHeight = pageHeight - showHeigtht; // 页面滚动了多少, 整个是随着页面滚动实时变化的 let nowScroll = $(document).scrollTop(); if ((canScrollHeight - nowScroll) < 100){ if(!bIsLoadData){ bIsLoadData = true; //判断页数,去更新新闻,小于总数才加载 if(iPage < iTotalPage){ iPage += 1; fn_load_content(); }else { message.showInfo('已全部加载,没有更多内容!'); $('a.btn-more').html('已全部加载,没有更多内容!') } } } }); // 向后端获取新闻列表数据 function fn_load_content() { $.ajax({ url: '/news/', type: 'GET', data:{ tag: iCurrentTagId, page: iPage }, dataType: 'json', success: function (res) { if(res.errno === '0'){ iTotalPage = res.data.total_pages; if(iPage === 1){ // 第一页清空内容 $('.news-list').html('') } res.data.news.forEach(function (one_news) { let content = ` <li class="news-item"> <a href="https://www.shiguangkey.com/course/2432" class="news-thumbnail" target="_blank"> <img src="${one_news.image_url}" alt="${one_news.title}" title="${one_news.title}"> </a> <div class="news-content"> <h4 class="news-title"><a href="#">${one_news.title}</a> </h4> <p class="news-details">${one_news.digest}</p> <div class="news-other"> <span class="news-type">${one_news.tag_name}</span> <span class="news-time">${one_news.update_time}</span> <span class="news-author">${one_news.author}</span> </div> </div> </li>`; $('.news-list').append(content); }); // $('.news-list').append($('<a href="javascript:void(0);" class="btn-more">滚动加载更多</a>')); //数据加载完毕,设置正在加载数据变量为false,表示当前没有加载数据 bIsLoadData = false; $('a.btn-more').html('滚动加载更多') }else { // 加载失败,打印错误信息 message.showError(res.errmsg) } }, error: function () { message.showError('服务器超时,请重试!') } }) } // 新闻轮播图功能 // 1.加载轮播图数据 function fn_load_banner() // 2.点击导航按钮切换 用this的话必须写function()了不能用()=> // 3.上一页,下一页 // 4.自动切换 // 5.鼠标滑入暂停自动播放 fn_load_banner(); // 先加载banner let $banner = $('.banner'); // banner容器div let $picLi = $('.banner .pic li'); // 图片li标签 let $pre = $('.banner .prev'); // 上一张 let $next = $('.banner .next'); // 下一张 let $tabLi = $('.banner .tab li'); // 按钮 let index = 0; // 当前索引 // 导航小圆点 $tabLi.click(function () { index = $(this).index(); $(this).addClass('active').siblings('li').removeClass('active'); $picLi.eq(index).fadeIn(1500).siblings('li').fadeOut(1500); }); // 点击切换上一张 $pre.click(()=> { index --; if(index<0){ index = $tabLi.length - 1 // 最后一张 } $tabLi.eq(index).addClass('active').siblings('li').removeClass('active'); $picLi.eq(index).fadeIn(1500).siblings('li').fadeOut(1500); }); // 点击切换下一张 $next.click(()=>{ auto(); }); // 图片向前滑动 function auto() { index ++; index %= $tabLi.length; $tabLi.eq(index).addClass('active').siblings('li').removeClass('active'); $picLi.eq(index).fadeIn(1500).siblings('li').fadeOut(1500) } // 定时器 let timer = setInterval(auto, 2500); $banner.hover( ()=>{ clearInterval(timer) }, ()=>{ timer = setInterval(auto, 2500); } ); // 定义向后端获取banner的ajax数据 function fn_load_banner() { $ .ajax({ url: '/news/banners/', type: 'GET', async: false, // 同步执行,下面的代码依赖banner的加载 dataType: "json", }) .done( (res)=> { if(res.errno === '0'){ let content = ''; let tab_content = ''; res.data.banners.forEach( (one_banner, index) =>{ if(index === 0){ // 第一页 加active属性 content = `<li style="display:block;"><a href="/news/${one_banner.news_id}/"> <img src="${one_banner.image_url}" alt="${one_banner.news_title}"></a></li>`; tab_content = '<li class="active"></li>'; }else { content = `<li><a href="/news/${one_banner.news_id}/"><img src="${one_banner.image_url}" alt="${one_banner.news_title}"></a></li>`; tab_content = '<li></li>'; } $('.pic').append(content); $('.tab').append(tab_content) }) }else { message.showError(res.errmsg) } }) .fail(()=>{ message.showError('服务器超时,请重试!') }) } });
1. 接口设计
-
接口说明:
类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | / |
参数格式 | 无参数 |
2.返回内容
返回新闻页面,直接在模板渲染
2.后端代码
视图代码
# 修改news/views.py中的index视图 def index(request): """ 新闻首页视图 url: / :param request: :return: """ tags = Tag.objects.only('id', 'name').filter(is_delete=False) hot_news = HotNews.objects.select_related('news').only('news__title', 'news__image_url', 'news_id').filter( is_delete=False ).order_by('priority', '-news__clicks')[:constants.SHOW_HOTNEWS_COUNT] return render(request, 'news/index.html', context={ 'tags': tags, 'hot_news': hot_news })
定义常量
# 在news/constants.py中定义下面的常量 # 显示热门新闻条数 SHOW_HOTNEWS_COUNT = 3
3.前端代码
<!-- 修改templates/news/index.html --> <ul class="recommend-news"> {% for item in hot_news %} <li> <a href="https://www.shiguangkey.com/course/2432" target="_blank"> <div class="recommend-thumbnail"> <img src="{{ item.news.image_url }}" alt="title"> </div> <p class="info">{{ item.news.title }}</p> </a> </li> {% endfor %} </ul> <!-- recommend-news end -->