python操作三大主流数据库(14)python操作redis之新闻项目实战②新闻数据的展示及修改、删除操作
项目目录:
├── flask_redis_news.py ├── forms.py ├── init_news.py ├── redis_news.py ├── static │ ├── bootstrap-3.3.7-dist │ │ ├── css │ │ ├── fonts │ │ └── js │ ├── bootstrap-3.3.7-dist.zip │ ├── datatables.min.css │ ├── datatables.min.js │ ├── img │ │ └── news │ ├── index.css │ ├── jquery-3.3.1.min.js │ └── main.css ├── templates │ ├── admin │ │ ├── add.html │ │ ├── admin_base.html │ │ ├── index.html │ │ └── update.html │ ├── cat.html │ ├── detail.html │ ├── home_base.html │ └── index.html
1.服务端代码flask_redis_news.py
#coding:utf-8 import sys defaultencoding = 'utf-8' if sys.getdefaultencoding() != defaultencoding: reload(sys) sys.setdefaultencoding(defaultencoding) import os from datetime import datetime from werkzeug import secure_filename from flask import Flask, request, render_template, redirect, flash, url_for, abort from flask_uploads import UploadSet, configure_uploads, IMAGES, patch_request_class from forms import NewsForm from redis_news import RedisNews app = Flask(__name__) app.config['SECRET_KEY'] = 'adfa@4314#31AD23#2' app.config['UPLOADED_PHOTOS_DEST'] = "/data/three_db_python/redis_version01/static/img/news" query = RedisNews() photos = UploadSet('photos', IMAGES) configure_uploads(app, photos) patch_request_class(app) # set maximum file size, default is 16MB @app.route("/", methods = ['GET']) def index(): ''' 获取新闻列表 ''' news_list = query.get_all_news() return render_template("index.html", news_list = news_list) @app.route("/cat/<name>/", methods = ['GET']) def cat(name): ''' 获取新闻列表 ''' news_list = query.get_news_from_cat(name) return render_template("cat.html", news_list = news_list) @app.route("/detail/<int:pk>/", methods = ['GET']) def detail(pk): ''' 获取新闻列表 ''' news_obj = query.get_news_from_id(pk) return render_template("detail.html", obj = news_obj) @app.route("/admin/", methods = ['GET']) @app.route("/admin/<int:page>/", methods = ['GET']) def admin(page = None): ''' 获取后台新闻列表 ''' if page is None: page = 1 page_data = query.paginate(page, 5) return render_template("admin/index.html", page_data = page_data) @app.route("/admin/add/", methods = ['GET','POST']) def admin_add(): ''' 从后台页面添加新闻 ''' form = NewsForm() news_obj = {} # 提交增加 if form.validate_on_submit(): # 图片文件名 filename = photos.save(form.photo.data) news_obj['title'] = form.title.data news_obj['news_type'] = form.news_type.data news_obj['img_url'] = form.img_url.data news_obj['content'] = form.content.data news_obj['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') news_obj['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') query.add_new_from_page(news_obj) flash('添加成功') return redirect(url_for('admin')) return render_template("admin/add.html", form = form) @app.route("/admin/update/<int:pk>/", methods = ['GET','POST']) def admin_update(pk): ''' 获取后台新闻列表 ''' # 获取新闻 news_obj = query.get_news_from_id(pk) if news_obj is None: abort('no this news') form = NewsForm(data = news_obj) # 提交修改 if form.validate_on_submit(): news_obj['title'] = form.title.data news_obj['news_type'] = form.news_type.data news_obj['img_url'] = form.img_url.data news_obj['content'] = form.content.data news_obj['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') query.update_news(pk, news_obj) flash('新闻修改成功') return redirect(url_for('admin')) return render_template("admin/update.html", form = form) @app.route("/admin/delete/<int:pk>/", methods = ['GET','POST']) def admin_delete(pk): ''' 删除新闻 ''' news_obj = query.get_news_from_id(pk) if news_obj: query.delete_news(pk, news_obj) return 'yes' return 'no' if __name__ == "__main__": app.run(debug = True,host="0.0.0.0")
2.辅助类新闻的具体操作redis_news.py
#coding:utf-8 import math import redis NEWS_FIELDS = ( "title", "img_url", "content", "is_valid", "news_type", "created_at", "updated_at" ) class RedisNews(object): def __init__(self): # 如果返回是二进制类似 b'3xe6x9cx885xe6x97xa5xe...'需要加decode_responses=True try: self.r = redis.StrictRedis(host = 'localhost', port=6379,encoding='utf-8', decode_responses=True, db=1) except Exception as e: print('redis connect faild') def _news_id(self, int_id): ''' 新闻id ''' return 'news:%d' % int(int_id) def _news_type(self, news_type): ''' 新闻类型 ''' return 'news_type:%s' % news_type def _news_list_name(self): ''' 新闻列表名称 ''' return 'news' def add_new_from_page(self, news_obj): ''' 从页面新增新闻数据 ''' # 记录news_id的数字自增1 int_id = self.r.incr('news_id', amount=1) # 获取新闻id news_id = self._news_id(int_id) # 新闻列表中添加 self.r.lpush(self._news_list_name(), int_id) # 新闻类型中添加信息 news_type = news_obj['news_type'] self.r.sadd(news_type, int_id) # 新闻内容中添加信息 self.r.hmset(news_id, news_obj) def add_news(self, news_obj): ''' 新增新闻数据 ''' # 获取到新闻的id int_id = int(self.r.incr('news_id')) # 拼接新闻数据Hash key(news:2) news_id = self._news_id(int_id) # 存储新闻数据(hash) rest = self.r.hmset(news_id, news_obj) # 存储新闻的id list self.r.lpush(self._news_list_name(), int(int_id)) # 存储新闻的类别-新闻id(set) news_type = _news_type(news_obj['news_type']) self.r.sadd(news_type, int_id) return rest def add_news_with_transaction(self, news_obj): ''' 使用事务来新增新闻数据 ''' pipe = self.r.pipeline(transaction=True) int_id = self.r.incr('news_id') news_id = self._news_id(int_id) # 使用列表list获取新闻的id pipe.lpush(self._news_list_name(), int_id) # 使用hash来保存新闻具体内容 pipe.hmset(news_id, news_obj) # 使用hash来保存新闻分类信息 news_type = self._news_type(news_obj['news_type']) pipe.sadd(news_type, int_id) rest = pipe.execute() return rest def get_all_news(self): ''' 获取所有新闻信息 ''' # 获取id列表 id_list = self.r.lrange(self._news_list_name(), 0, -1) data_list = [] for int_id in id_list: # 获取具体新闻内容 news_id = self._news_id(int_id) data = self.r.hgetall(news_id) data['id'] = int_id # print(data) data_list.append(data) return data_list def get_news_from_id(self, news_id): ''' 根据新闻id获取新闻内容 ''' news_id = self._news_id(news_id) # 根据新闻id获取新闻内容 news_obj = self.r.hgetall(news_id) return news_obj def get_news_from_cat(self, cat_name): ''' 根据新闻类型获取新闻内容 ''' news_list = [] # 获取新闻类型 news_type = self._news_type(cat_name) # print(news_type) # 获取新闻类型集合中新闻id的列表 id_list = self.r.smembers(news_type) print(id_list) for int_id in id_list: # 获取新闻id news_id = self._news_id(int_id) # 根据新闻id获取新闻内容 data = self.r.hgetall(news_id) data['id'] = int_id news_list.append(data) return news_list def update_news(self, pk, news_obj): ''' 新闻的修改 ''' news_id = self._news_id(pk) # 修改新闻 rest = self.r.hmset(news_id, news_obj) return rest def delete_news(self, pk, news_obj): ''' 新闻的删除,物理删除 关于常用的方法可以通过查询redis的命令类型判断是list,string还是hash或者set 1.命令列表定位到具体命令:http://www.redis.cn/commands.html#hash 2.找到命令后,查询api的用法http://redis-py.readthedocs.io/en/latest/ ''' # 获取新闻id news_id = self._news_id(pk) # 从新闻列表中删除新闻id self.r.lrem(self._news_list_name(), 0, pk) # 从新闻的类型set集合中清理新闻id news_type = self._news_type(news_obj['news_type']) self.r.srem(news_type, pk) # 从新闻的内容hash列表中清理具体的新闻内容NEWS_FIELDS(具体的列信息) self.r.hdel(news_id, *NEWS_FIELDS) def paginate(self, page=1, per_page=5): ''' 新闻后台分页 ''' if page is None: page = 1 data_list = [] # 开始页,结束页面 start = (page - 1)*per_page end = page*per_page - 1 # 获取所有新闻列表(计算页码使用) list_ids = self.r.lrange(self._news_list_name(), 0, -1) # 获取新闻列表 id_list = self.r.lrange(self._news_list_name(), start, end) # print('id_list%s' % id_list) for int_id in id_list: news_id = self._news_id(int_id) # 根据新闻id获取新闻内容 data = self.r.hgetall(news_id) data['id'] = int_id data_list.append(data) # print('data_list%s' % data_list) return Pagenation(data_list, page, per_page, list_ids) def init_news(self, data_list): ''' 批量导入新闻数据 ''' for news_obj in data_list: rest = self.add_news_with_transaction(news_obj) print(rest) class Pagenation(object): ''' 分页类 ''' def __init__(self, data_list, now_page, per_page, list_ids): self.now_page = now_page self.data_list = data_list self.per_page = per_page self.list_ids = list_ids @property def page(self): ''' 当前页 ''' return self.now_page @property def items(self): ''' 返回页面数据 ''' return self.data_list @property def prev_num(self): ''' 上一页 ''' return self.now_page - 1 @property def next_num(self): ''' 下一页页码 ''' return self.now_page + 1 @property def has_prev(self): ''' 是否有上一页 ''' return self.now_page > 1 @property def has_next(self): ''' 是否有下一页 ''' return self.per_page == len(self.data_list) def iter_pages(self): ''' 页码 ''' # 获取所有的id长度(即新闻条数)除以每页显示的页面,得到取进一位的整数 total_page = math.ceil(len(self.list_ids)/self.per_page) # print('total_page=%d' % total_page) return range(1, total_page)
3.表单类forms.py
from flask_wtf import FlaskForm from wtforms import StringField, TextAreaField, SubmitField, SelectField from flask_wtf.file import FileField, FileRequired, FileAllowed from wtforms.validators import DataRequired class NewsForm(FlaskForm): """新闻表单数据验证""" title = StringField(label = '新闻标题', validators = [DataRequired('请输入标题')], description = '请输入标题', render_kw={'required':'required', 'class':'form-control'}) content = TextAreaField(label = '新闻内容', validators = [DataRequired('请输入新闻内容')], description = '请输入新闻内容', render_kw={'required':'required', 'class':'form-control'}) news_type = SelectField('新闻类型', choices = [('推荐','推荐'), ('百家', '百家'),('本地','本地'), ('图片','图片')]) img_url = StringField(label='新闻图片', description='请输入图片地址', render_kw={'required':'required', 'class':'form-control'}) photo = FileField('图片上传', validators=[FileAllowed(['png', 'JPEG', 'jpg'], '只能上传图片!'), FileRequired('文件未选择!')]) submit = SubmitField('提交')
4.前台展示页面
①模板home_base.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="{{ url_for('static', filename='bootstrap-3.3.7-dist/css/bootstrap.min.css')}}"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css')}}"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='datatables.min.css')}}"> <script type="text/javascript" src="{{ url_for('static', filename='jquery-3.3.1.min.js')}}"></script> <script type="text/javascript"> $(document).ready(function() { $('#example').DataTable(); } ); </script> {% block head %} <title>首页</title> {% endblock %} </head> <body> <div class="container"> <h1>新闻列表</h1> <nav class="navbar navbar-inverse"> <!-- 页面头部 --> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-menu" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> </div> <div id="navbar-menu" class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li class="active"><a href="/">首页</a></li> <li><a href="{{url_for('cat', name='推荐')}}">推荐</a></li> <li><a href="{{url_for('cat', name='百家')}}">百家</a></li> <li><a href="{{url_for('cat', name='本地')}}">本地</a></li> <li><a href="{{url_for('cat', name='图片')}}">图片</a></li> </ul> </div> </nav> <!-- 新闻内容部分 --> {% block content %} <!-- 内容区域 --> {% endblock %} </div> {% block extrajs %} <!-- 其他脚本 --> {% endblock %} </body> </html>
②首页index.html
{% extends 'home_base.html' %} {% block content%} <div id="content" class="row-fluid"> <div class="col-md-12"> <table id="example" class="table table-striped table-bordered" cellspacing="0" width="100%"> <thead> <tr> <th>图片</th> <th>简介</th> </tr> </thead> <tbody> {% for obj in news_list %} <tr> <td> <img width=120 height=60 src="{{ obj.img_url }}" alt="图片"> </td> <td> <p> <a href="{{ url_for('detail', pk=obj.id) }}">{{ obj.title }}</a> <small>{{ obj.created_at }}</small> </p> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> {% endblock %} {% block extrajs %} <script type="text/javascript" src="{{ url_for('static', filename = 'datatables.min.js')}}"></script> {% endblock %}
③详情页detail.html
{% extends 'home_base.html' %} {% block head %} <title>新闻详情</title> {% endblock %} {% block content%} <div id="content" class="row-fluid"> <div class="col-md-9"> <h2>新闻详情,来自新闻id> {{obj.id}}</h2> </div> <div class="col-md-12"> <table id="example" class="table table-striped table-bordered" cellspacing="0" width="100%"> <thead> <tr> <th>{{ obj.content }}</th> </tr> </thead> <tbody> <tr> <td> <img width=600 height=500 src="{{ obj.img_url }}" alt="图片"> </td> <td> </tr> <tr> <td> <p> {{ obj.title }} <small>{{ obj.created_at }}</small> </p> </td> </tr> </tbody> </table> </div> </div> </div> {% endblock %} </body> </html>
④分类页面cat.html
{% extends 'home_base.html' %} {% block head%} <title>{{ name }}</title> {% endblock %} {% block content%} <div id="content" class="row-fluid"> <div class="col-md-12"> <table id="example" class="table table-striped table-bordered" cellspacing="0" width="100%"> <thead> <tr> <th>图片</th> <th>简介</th> </tr> </thead> <tbody> {% for obj in news_list %} <tr> <td> <img width=120 height=60 src="{{ obj.img_url }}" alt="图片"> </td> <td> <p> <a href="{{ url_for('detail', pk=obj.id) }}">{{ obj.title }}</a> <small>{{ obj.created_at }}</small> </p> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> {% endblock %} {% block extrajs %} <script type="text/javascript" src="{{ url_for('static', filename = 'datatables.min.js')}}"></script> {% endblock %}
5.后台管理页面
①后台模板页面admin/admin_base.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='bootstrap-3.3.7-dist/css/bootstrap.min.css')}}"> {% block head %} <title>首页</title> {% endblock %} </head> <body> <!-- 导航栏 --> <div class="container"> <div class="row"> <div class="bs-example" data-example-id="default-navbar"> <nav class="navbar navbar-default"> <div class="container-fluid"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="{{ url_for('admin_add')}}">添加新闻</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li> <li><a href="#">Link</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="#">Action</a></li> <li><a href="#">Another action</a></li> <li><a href="#">Something else here</a></li> <li role="separator" class="divider"></li> <li><a href="#">Separated link</a></li> <li role="separator" class="divider"></li> <li><a href="#">One more separated link</a></li> </ul> </li> </ul> <form class="navbar-form navbar-left"> <div class="form-group"> <input type="text" class="form-control" placeholder="Search"> </div> <button type="submit" class="btn btn-default">Submit</button> </form> <ul class="nav navbar-nav navbar-right"> <li><a href="#">Link</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="#">Action</a></li> <li><a href="#">Another action</a></li> <li><a href="#">Something else here</a></li> <li role="separator" class="divider"></li> <li><a href="#">Separated link</a></li> </ul> </li> </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> <!-- 新闻内容部分 --> {% block content %} <!-- 内容区域 --> {% endblock %} </div> {% block extrajs %} <!-- 其他脚本 --> {% endblock %} </body> </html>
②后台首页admin/index.html
{% extends 'admin/admin_base.html' %} {% block head %} <title>新闻后台首页</title> {% endblock %} {% block content %} <!-- 消息闪现 --> {% for msg in get_flashed_messages() %} <p class="bg-success">{{msg}}</p> {% endfor %} <!-- 表格,存放新闻具体内容 --> <table class="table table-hover"> <tr class="info"> <th>编号</th> <th>新闻标题</th> <th>类别</th> <th>添加时间</th> <th>操作</th> </tr> {% for new_obj in page_data.items %} <tr class="active"> <td>{{ new_obj.id }}</td> <td>{{ new_obj.title }}</td> <td>{{new_obj.types }}</td> <td>{{new_obj.created_at }}</td> <td><a href="{{url_for('admin_update', pk = new_obj.id)}}" class='btn btn-success'>修改</a><a data-url="{{ url_for('admin_delete', pk=new_obj.id) }}" class='btn btn-danger'>删除</a></td> </tr> {% endfor %} </table> <!-- 分页,默认分页 --> <nav aria-label="Page navigation"> <ul class="pagination"> {% if page_data.has_prev %} <li> <a href="{{ url_for('admin', page=page_data.prev_num) }}" aria-label="Previous"> <span aria-hidden="true">«</span> </a> </li> {% else %} <li class="disabled"><a href="javascipt:;">»</a></li> {% endif %} {% for page in page_data.iter_pages() %} {% if page == page_data.page %} <li class="active"> <a href="javascript:;">{{ page }}</a> </li> {% else %} <li> <a href="{{ url_for('admin', page=page) }}">{{ page }}</a> </li> {% endif %} {% endfor %} {% if page_data.has_next %} <li> <a href="{{ url_for('admin', page=page_data.next_num) }}">»</a> </li> {% else %} <li class="disabled"> <a href="javascript:;">»</a> </li> {% endif %} </ul> </nav> </div> </div> {% endblock %} {% block extrajs %} <script type="text/javascript" src="{{ url_for('static', filename='jquery-3.3.1.min.js') }}"></script> <script type="text/javascript"> // 通过ajax异步删除新闻 $(function(){ $('.btn-danger').on('click', function(){ var _this = $(this) var url = _this.attr('data-url') // 弹框确认是否删除 if (confirm('确认删除吗?')){ // ajax发送post请求 $.post(url, function(res){ if(res == 'yes'){ // 如果后台删除成功则隐藏该行 _this.parents('tr').hide() }else{ alert('删除失败'); } }) } }) }) </script> {% endblock %} </body> </html>
③后台添加页面admin/add.html
{% extends 'admin/admin_base.html' %} {% block head %} <title>新闻添加页面</title> {% endblock %} {% block content %} <!-- 添加新闻内容 --> <form action="/admin/add/" method="post" enctype="multipart/form-data"> <div class="form-group"> <label for="exampleInputEmail1">{{ form.title.label.text }}</label> <input type="text" name="title" class="form-control" id="exampleInputEmail1" placeholder="news title"> </div> <div class="form-group"> <label for="exampleInputPassword1">{{ form.news_type.label.text }}</label> <div> {{ form.news_type }} </div> </div> <div class="form-group"> {{ form.photo }} {% for error in form.photo.errors %} <span style="color: red;">{{ error }}</span> {% endfor %} <p class="help-block">新闻图片上传</p> </div> <div class="form-group"> <p class="help-block">新闻图片的路径</p> {{ form.img_url }} </div> <div class="form-group"> {{ form.content }} </div> <br> {{ form.csrf_token }} {{ form.submit }} </form> {% endblock %} </body> </html>
④后台新闻更新页面admin/update.html
{% extends 'admin/admin_base.html' %} {% block head %} <title>修改新闻</title> {% endblock %} {% block content %} <form role='form' class="form-horizontal" method="post"> <div class="form-group"> <label for="exampleInputEmail1">{{ form.title.label.text }}</label> <div> {{form.title}} </div> </div> <div class="form-group"> <label for="exampleInputPassword1">{{ form.news_type.label.text }}</label> <div> {{ form.news_type }} </div> </div> <div class="form-group"> <label for="exampleInputFile">{{ form.img_url.label.text }}</label> <input type="file" name="image" id="exampleInputFile"> {{ form.img_url }} <!-- <p class="help-block">新闻图片上传</p> --> </div> <div class="form-group"> {{ form.content }} </div> <br> {{ form.csrf_token }} {{ form.submit }} </form> {% endblock %} </body> </html>