f9.4 新闻展示


远程仓库地址
https://gitee.com/cz_zzz/InfoNews24

一.新闻首页

1.排行列表(重点)

  • 接口分析
    渲染方式: 全局刷新/SEO -->后端渲染–>跟路由
    数据库行为:查询点击量排行前10的新闻
  • 代码实现
    在首页路由中, 查询点击量排行前10的新闻
    将排行数据传入模板渲染
index.html:
<ul class="rank_list">
            {% for news in rank_list %}
                <li><span class="{{ loop.index | index_convert }}">{{ loop.index }}</span><a href="/news/1">{{ news.title }}</a></li>
            {% endfor %}
        </ul>
views.py:
# 查询 点击量排行前10的新闻
    try:
        rank_list = News.query.order_by(News.clicks.desc()).limit(10).all()
    except BaseException as e:
        current_app.logger.error(e)
        return abort(500)

    user = user.to_dict() if user else None
    # 将新闻排行数据 传入模板渲染
    return render_template("index.html", user=user, rank_list=rank_list)
  • 使用自定义过滤器来设置排行样式
index.html:
                <li><span class="{{ loop.index | index_convert }}">{{ loop.index }}</span><a href="/news/1">{{ news.title }}</a></li>
common.py:
# 定义自定义过滤器
def func_index_convert(index):  # 1.定义形参接收模板变量
    index_dict = {1: "first", 2: 'second', 3: "third"}
    return index_dict.get(index, "")  # 2.将转换结果返回

init.py:
# 让应用添加过滤器
    from info.utils.common import func_index_convert
    app.add_template_filter(func_index_convert, "index_convert")

2.获取新闻列表(重点)

  • 接口分析
    渲染方式:局部刷新–>前端渲染–>定义路由,返回数据
    数据库行为:按照分类和页码来查询(get)新闻 按发布时间倒叙排列

  • 接口文档

获取新闻列表
/get_news_list(新建)
请求方式
GET
请求参数
cid 分类id
cur_page  当前页码
per_count  每页条数
响应形式
json
  • 代码实现
# 获取新闻列表
@home_blu.route('/get_news_list')
def get_news_list():
    # 获取参数
    cid = request.args.get("cid")
    cur_page = request.args.get("cur_page")
    per_count = request.args.get("per_count", HOME_PAGE_MAX_NEWS)
    # 校验参数
    if not all([cid, cur_page]):
        return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])
    try:
        cid = int(cid)
        cur_page = int(cur_page)
        per_count = int(per_count)
    except BaseException as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])

    # 数据库行为:  按照分类和页码查询新闻  按发布时间倒序
    try:  
          pn = News.query.filter_by(category_id=cid).order_by(News.create_time.desc()).paginate(cur_page, per_count)
    except BaseException as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg=error_map[RET.DBERR])

    data = {
        "news_list": [news.to_dict() for news in pn.items],
        "total_page": pn.pages  # 前端需要根据总页数来判断是否还需要请求
    }
    # 返回json结果
    return jsonify(errno=RET.OK, errmsg=error_map[RET.OK], data=data)

tips:

  1. 排序时先按时间排好后再分页
  2. jsonify返回前先进行格式转换:
    "news_list": [news.to_dict() for news in pn.items],
    tips: 存在问题—>最新一项没有展示
  • 分类”最新”
# 数据库行为:  按照分类和页码查询新闻  按发布时间倒序
    try:  # 如果是"最新", 所有新闻一起分页排序
        if cid == 1:
            pn = News.query.order_by(News.create_time.desc()).paginate(cur_page, per_count)
        else:  # 如果为其他分类, 根据cid进行筛选
            pn = News.query.filter_by(category_id=cid).order_by(News.create_time.desc()).paginate(cur_page, per_count)
    except BaseException as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg=error_map[RET.DBERR])

3优化

1.过滤 分类/最新

# 数据库行为:  按照分类和页码查询新闻  按发布时间倒序
filter_list = []
if cid != 1:
    filter_list.append(News.category_id==cid)  #  filter_list = [News.category_id==cid]

try:  # 如果是"最新", 所有新闻一起分页排序
    pn = News.query.filter(*filter_list).order_by(News.create_time.desc()).paginate(cur_page, per_count)

    # if cid == 1:
    #     pn = News.query.order_by(News.create_time.desc()).paginate(cur_page, per_count)
    # else:  # 如果为其他分类, 根据cid进行筛选
    #     pn = News.query.filter(News.category_id==cid).order_by(News.create_time.desc()).paginate(cur_page, per_count)

except BaseException as e:
    current_app.logger.error(e)
    return jsonify(errno=RET.DBERR, errmsg=error_map[RET.DBERR])

tips:注释部分为原来思路,本次考虑用实参前面加*–>类似于解包---->如果没有值,则结果是""(空字符串)

2.新闻分类列表

  • 接口分析:
    渲染方式: 全局刷新–>后端渲染–> 跟路由
    数据库行为: 查询分类数据

  • demo实现

      # 查询所有的分类数据
      try:
      categories = Category.query.all()
      except BaseException as e:
      current_app.logger.error(e)
      return abort(500)
    
      user = user.to_dict() if user else None
      # 将新闻排行数据 和 分类数据 传入模板渲染
      return render_template("index.html", user=user, rank_list=rank_list, categories=categories)
    

    tips:注意要在return处返回categories=categories

    • 模板渲染

          {#  TODO 设置data-cid为分类的id #}
          {% for category in categories %}
              <li data-cid="{{ category.id }}" {% if loop.index == 1 %}class="active"{% endif %}><a href="javascript:;">{{ category.name }}</a></li>
          {% endfor %}
        
          {#            <li class="active" data-cid="1"><a href="javascript:;">最新</a></li>#}
          {#            <li data-cid="2"><a href="javascript:;">股市</a></li>#}
          {#            <li data-cid="3"><a href="javascript:;">债市</a></li>#}
          {#            <li data-cid="4"><a href="javascript:;">商品</a></li>#}
          {#            <li data-cid="5"><a href="javascript:;">外汇</a></li>#}
          {#            <li data-cid="6"><a href="javascript:;">公司</a></li>#}
        

    tips:data-cid="{{ category.id }}"用于构建获取新闻列表的请求参数

    • 优化首页展示
      1.<a href="#" class="logo fl">------><a href="{{ url_for("home.index") }}" class="logo fl">------->logo展示
      2.`
        `------>删除了原来样本中的示例数据

      二.新闻详情

      1.显示详情页面(重点)

      • 接口分析
        渲染方式:全局刷新/SEO–>后端渲染—>新定义路由(modules-->views),渲染详情页面
        数据库行为:查询某条新闻数据—>前端需要传递新闻的id

      • 接口文档
        新闻详情
        /news/<news_id> ------->采用动态URL传递新闻id的方式
        请求方式
        GET
        请求参数

        响应形式
        html
        tips:渲染方式---->响应形式;数据库的行为----->请求参数---->请求方式

      • 代码实现
        views界面:

          from flask import current_app, abort, render_template
        
          from info.modules.news import news_blu
          from info.utils.models import News
        
        
          @news_blu.route('/<int:news_id>')  # 新闻详情
          def news_detail(news_id):
              # 根据新闻id查询新闻模型
              try:
          	news = News.query.get(news_id)
              except BaseException as e:
          	current_app.logger.error(e)
          	return abort(500)
        
              # 将新闻数据传入模板渲染
              return render_template("detail.html", news=news.to_dict())
        
      • 模板渲染

          	<h3>{{ news.title }}</h3>
          		    <div class="detail_about clearfix">
          			<span class="time_souce fl">{{ news.create_time }} 来源: {{ news.source }}</span>
          			<span class="comment fr">0</span>
          		    </div>
          		    {{ news.content | safe }}   {# jinja2对html字符进行自动转义,可以使用safe取消转义 #}
        

      tips:

      1. 在`templates–>details里做渲染
      2. {{ news.content | safe }} —>{# jinja2对html字符进行自动转义,可以使用safe取消转义 #}

      2.抽取基类

      • 抽取原则:
        1. 父子不同,父类定义代码块,子类重写
        2. 父子相同,子类直接继承
      • 抽取方法:新建base.html,分别与detail和index进行对比

      3.显示其他数据

      由于home中的首页数据传到了index.html进行渲染,而基类里并没有提留存
      故:
      在详情页(news)的views要对detail进行传参进行渲染
      f9.4 新闻展示

      • 装饰器方式封装数据查询---->抽取函数和采用装饰器均可
        --------对于相同的展示页面可以提取为函数
        f9.4 新闻展示

      • 装饰器bug
        1. flask中不允许同一个蓝图定义的路由的函数标记相同,一旦相同就会报错
        2. 使用functools.wraps装饰器修改闭包函数的函数信息
        解决方法:

          	@functools.wraps(f)  # 此处为Python内置装饰器(非flask装饰器)可以让被装饰的函数使用指定函数的函数信息(函数名__name__, 函数文档__doc__):
        
      1. url_map指向示例f9.4 新闻展示
        浏览器提示重复指向:
        f9.4 新闻展示
      2. flask指向修改f9.4 新闻展示
      3. 多个装饰器引发的指向问题f9.4 新闻展示
      4. @functools.wraps(f)修改f9.4 新闻展示
      5. 修改结果

      f9.4 新闻展示

      三.收藏新闻

      1.收藏/取消收藏(重点)

      • 接口分析
        1. 渲染方式: 局部刷新–> 前端渲染 -->定义新路由(news/news_collect),实现新闻收藏(收藏与否通过两个<a>标签来实现)
        2. 数据库行为: 使用外键(关系属性)来关联用户和收藏的新闻

      • 接口文档
        新闻收藏/取消收藏–>/news/news_collect
        请求方式 —>POST/json
        请求参数 news_id —>新闻id// action 行为 collect/cancel_collect
        响应形式 —> json

      • demo

          			# 新闻收藏
          			@news_blu.route('/news_collect', methods=['POST'])
          			@user_login_data
          			def news_collect():
          			    user = g.user  # 获取登录的用户信息
          			    if not user:  # 用户未登录
          				return jsonify(errno=RET.SESSIONERR, errmsg=error_map[RET.SESSIONERR])
          			
          		    # 获取参数
          		    news_id = request.json.get("news_id")
          		    action = request.json.get("action")
          		    # 校验参数
          		    if not all([news_id, action]):
          			return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])
        
          		    if action not in ["collect", "cancel_collect"]:
          			return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])
          		    
          		    try:
          			news_id = int(news_id)
          		    except BaseException as e:
          			current_app.logger.error(e)
          			return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])
        
          		    # 查询新闻模型
          		    try:
          			news = News.query.get(news_id)
          		    except BaseException as e:
          			current_app.logger.error(e)
          			return jsonify(errno=RET.DBERR, errmsg=error_map[RET.DBERR])
        
          		    # 数据库行为: 使用关系属性 来关联用户和收藏的新闻
          		    if action == "collect":  # 收藏
          			user.collection_news.append(news)
          		    else:  # 取消收藏
          			user.collection_news.remove(news)
        
          		    # json返回结果
          		    return jsonify(errno=RET.OK, errmsg=error_map[RET.OK])
        

        tips: 采用g变量来进行传递数据

      • 模板渲染---->data-newid="{{news.id}}":用于构建新闻收藏时的请求参数

      2.显示收藏情况(重点)

      • 接口分析
        1. 渲染方式: 全局刷新—>后端渲染–>在详情路由中,渲染页面

        2. 数据库行为:查询新闻是否被当前用户收藏
          demo:

           is_collected = False  # 记录是否收藏了该新闻
           if user:  # 判断用户是否登录
           # 查询新闻是否被当前用户收藏
           if news in user.collection_news:
               is_collected = True
          
           user = user.to_dict() if user else None
           # 将新闻数据传入模板渲染
           return render_template("detail.html", news=news.to_dict(), user=user, rank_list=rank_list, is_collected=is_collected)
          

      tips:利用is_collected = False来传递是否收藏信息,后边也会用到类似变量的传递渲染所需的模板变量(is_collected=is_collected)

      • 模板渲染

          <!-- TODO 未收藏设置display为block/已收藏为none, data-newsid为新闻id-->
          <a href="javascript:;" class="collection block-center" data-newid="{{ news.id }}" style="display: {% if is_collected %}none{% else %}block{% endif %};">收藏</a>
          <!-- TODO 未收藏设置display为none/已收藏为block, data-newsid为新闻id-->
          <a href="javascript:;" class="collected block-center" data-newid="{{ news.id }}" style="display: {% if is_collected %}block{% else %}none{% endif %};">
        

      tips:

      1. TODO注释---->方便前后端的交流
      2. 上边的两个标签是互斥行为,仅在同一位置显示一个,欲知详情可以查看css文件

      四.评论/回复

      基本实现思想和收藏/显示收藏一致,需要区*部刷新(2)和页面刷新(3)

      1.显示评论框

      demo:

      	<!-- TODO 未收藏设置display为block/已收藏为none, data-newsid为新闻id-->
          <a href="javascript:;" class="collection block-center" data-newid="{{ news.id }}"
             style="display: {% if is_collected %}none{% else %}block{% endif %};">收藏</a>
          <!-- TODO 未收藏设置display为none/已收藏为block, data-newsid为新闻id-->
          <a href="javascript:;" class="collected block-center" data-newid="{{ news.id }}"
             style="display: {% if is_collected %}block{% else %}none{% endif %};">
              <span class="out">已收藏</span><span class="over">取消收藏</span></a>
      
      
          {% if user %}  <!-- TODO 已登录显示上边, 隐藏下边,  data-newsid为新闻id-->
      
              <form action="" class="comment_form" data-newsid="{{ news.id }}">
                  <div class="person_pic">
                      <img src="../static/news/images/cat.jpg" alt="用户图标">
                  </div>
                  <textarea placeholder="请发表您的评论" class="comment_input"></textarea>
                  <input type="submit" name="" value="评 论" class="comment_sub">
              </form>
      
          {% else %}
      
              <div class="comment_form_logout">
                  登录发表你的评论
              </div>
      
          {% endif %}
      
      • 显示评论数量
        评论数量在新闻的详情页展示,故可以直接在news/detail里面做渲染:
        {{ news.comments_count }}—>中间有to_dict的转换字典格式

      • 显示当前新闻的所有评论可在model的评论里添加条目:
        comments = db.relationship("Comment", lazy="dynamic")

      • 自关联多对一关系属性

          父评论id
          # 自关联多对一关系属性, 需要设置remote_side=[主键名]
          # parent = db.relationship("Comment", remote_side=[id])
          # children = db.relationship("Comment")  # 一对多
          # parent = db.relationship('Comment', remote_side=[id])  # 多对一
          children = db.relationship("Comment", backref=db.backref('parent', remote_side=[id]))
        

      2.评论/回复基本实现(重点)

      • 接口分析
        渲染方式:局部刷新—>前端渲染---->定义路由,实现评论
        数据库行为:增加一条评论数据

      • 接口文档
        新闻评论/子评论
        /news/news_comment
        请求方式
        POST
        请求参数
        comment 评论内容
        news_id 新闻id
        parent_id 父评论id ---->子评论需要参数展示
        响应形式
        json
        tips:需要注意的是子评论与父评论的一对多情况是需要跟据页面详情展示所决定的,此处为一子评论对多父评论.

      • 代码实现

          # 新闻评论
          @news_blu.route('/news_comment', methods=['POST'])
          @user_login_data
          def news_comment():
              user = g.user
              if not user:  # 用户未登录
          	return jsonify(errno=RET.SESSIONERR, errmsg=error_map[RET.SESSIONERR])
        
              # 获取参数
              comment_content = request.json.get("comment")
              news_id = request.json.get("news_id")
              parent_id = request.json.get("parent_id")
        
              # 校验参数
              if not all([comment_content, news_id]):
          	return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])
        
              try:
          	news_id = int(news_id)
              except BaseException as e:
          	current_app.logger.error(e)
          	return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])
        
              # 数据库行为: 增加一条评论数据
              comment = Comment()
              comment.content = comment_content
              comment.news_id = news_id
              comment.user_id = user.id
              if parent_id:  # 传parent_id, 说明是子评论
          	try:
          	    parent_id = int(parent_id)
          	    comment.parent_id = parent_id
          	except BaseException as  e:
          	    current_app.logger.error(e)
          	    return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])
        
              try:
          	db.session.add(comment)
          	db.session.commit()  # 为了在视图函数中生成评论id, 必须手动提交(自动提交在视图函数执行完才会生成评论id)
              except BaseException as e:
          	current_app.logger.error(e)
          	return jsonify(errno=RET.DBERR, errmsg=error_map[RET.DBERR])
        
              # json返回数据  需要将评论的id也返回(用于前端发子评论时, 构建请求参数parent_id)
              return jsonify(errno=RET.OK, errmsg=error_map[RET.OK], data=comment.to_dict())
        

      tips:

      	1. db.session.commit()  # 为了在视图函数中生成评论id, 必须手动提交(自动提交在视图函数执行完才会生成评论id)-------->data=comment.to_dict()
      	2. 因篇幅有限,数据库的操作未做rool_back处理
      

      3.显示评论情况(重点)

      • 接口分析
        渲染方式:全局刷新---->后端渲染---->详情路由中,模板渲染
        数据库行为:查询该新闻的所有评论

      • 代码实现

          # 查询该新闻的所有评论  按照发布时间倒序排列
          try:
          comments = news.comments.order_by(Comment.create_time.desc()).all()
          except BaseException as e:
          current_app.logger.error(e)
          return abort(500)
        
          user = user.to_dict() if user else None
          # 将新闻数据传入模板渲染
          return render_template("detail.html", news=news.to_dict(), user=user, rank_list=rank_list, is_collected=is_collected, comments=[comment.to_dict() for comment in comments])
        

      tips:comments=[comment.to_dict() for comment in comments]---->列表推导式,区分三元运算符

      • 模板渲染
        tips:
      1. 区分{{ comment.user.nick_name }}和{{ comment.parent.user.nick_name }}
      # 定义自关联多对一关系属性时, 必须设置形参remote_side=[主键]
      # children = db.relationship("Comment")  # 一对多
      # parent = db.relationship('Comment', remote_side=[id])  # 多对一
      
      children = db.relationship("Comment", backref=db.backref('parent', remote_side=[id]))