编写 TODO 应用【part001】

本书前面两个部分分别对 Flask 的基本知识、用法以及介绍了多种扩展以及扩展的通用使用方式,使用扩展过程中的一些细节进行了讲解。虽然过程中有一个 REST API 小例子描述,但是,毕竟是作为各个扩展使用讲解而编排在一块,所以缺乏系统性,全局性。

从本章开始,我将使用 Flask 围绕一个 TODO 应用提供 REST API 进行讲解,让大家有个对 Flask 应用有一个直观的认识。

TODO 应用讲解

我们需要编写的 TODO 应用主要功能有:

  • 可以查询所有待办事项
  • 可以查看指定待办事项的详情
  • 可以增加一项待办事项
  • 可以删除一项待办事项
  • 可以修改一项待办事项,包括待办内容,添加标记
  • 完成待办事项后可以标记为完成

这些就是我们应用的简略需求,然后再讲一下我们的项目结构,根据前面章节《更好得维护代码》中讲解的,我们将项目结构设计成如下:

  1. .
  2. ├── README.md
  3. ├── application
  4. ├── __init__.py
  5. ├── controllers
  6. ├── __init__.py
  7. ├── auth.py
  8. ├── todo.py
  9. └── user.py
  10. ├── extensions.py
  11. └── models
  12. ├── __init__.py
  13. ├── todo.py
  14. └── user.py
  15. ├── commands.py
  16. ├── config
  17. ├── __init__.py
  18. ├── default.py
  19. ├── development.py
  20. ├── development_sample.py
  21. ├── production.py
  22. ├── production_sample.py
  23. └── testing.py
  24. ├── deploy
  25. ├── flask_env.sh
  26. ├── gunicorn.conf
  27. ├── nginx.conf
  28. └── supervisor.conf
  29. ├── manage.py
  30. ├── pylintrc
  31. ├── requirements.txt
  32. ├── tests
  33. └── __init__.py
  34. └── wsgi.py

设计 Models

Model 的话主要设计两个主要的模型,分别是 User 和 Item。User 表示用户的信息,除了表示TODO 所属人之外,还有登录的用处,而 Item 则是待办事项了,具体设计需要参考需求而定,关于 Model 的具体设计过程不是本章讨论的重点,所以直接给出 Models:

application/models/init.py

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. from user import *
  4. from todo import *
  5. def all():
  6. result = []
  7. models = [user, todo]
  8. for m in models:
  9. result += m.__all__
  10. return result
  11. __all__ = all()

application/models/user.py

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. from application.extensions import db
  4. __all__ = ['Role', 'User']
  5. class Permission:
  6. READ = 0x01
  7. CREATE = 0x02
  8. UPDATE = 0x04
  9. DELETE = 0x08
  10. DEFAULT = READ
  11. class Role(db.Document):
  12. name = db.StringField()
  13. permission = db.IntField()
  14. class User(db.Document):
  15. name = db.StringField()
  16. password = db.StringField()
  17. email = db.StringField()
  18. role = db.ReferenceField('Role')
  19. def to_json(self):
  20. return {"name": self.name,
  21. "email": self.email,
  22. "role": self.role.name}
  23. def is_authenticated(self):
  24. return True
  25. def is_active(self):
  26. return True
  27. def is_anonymous(self):
  28. return False
  29. def get_id(self):
  30. return str(self.id)

application/models/todo.py

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. from application.extensions import db
  4. __all__ = ['Item']
  5. class Item(db.Document):
  6. content = db.StringField(required=True)
  7. created_date = db.DateTimeField()
  8. completed = db.BooleanField(default=False)
  9. completed_date = db.DateTimeField()
  10. created_by = db.ReferenceField('User', required=True)
  11. notes = db.ListField(db.StringField())
  12. priority = db.IntField()
  13. def __repr__(self):
  14. return "<Item: {} Content: {}>".format(str(self.id),
  15. self.content)
  16. def to_json(self):
  17. return {
  18. 'id': str(self.id),
  19. 'content': self.content,
  20. 'completed': self.completed,
  21. 'completed_at': self.completed_date.strftime("%Y-%m-%d %H:%M:%S") if self.completed else "",
  22. 'created_by': self.created_by.name,
  23. 'notes': self.notes,
  24. 'priority': self.priority
  25. }

设计 views

根据我们在前面章节所学习的知识,我们这个应用的 views 就不是直接使用 app.route 来绑定 URL 了,而是使用 Blueprint 来设计,具体设计如下:

application/controller/init.py

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. import auth
  4. import user
  5. import todo
  6. all_bp = [
  7. auth.auth_bp,
  8. user.user_bp,
  9. todo.todo_bp
  10. ]

application/controller/auth.py

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. import json
  4. from flask import Blueprint, request, jsonify
  5. from flask.ext.login import login_user, logout_user
  6. import application.models as Models
  7. auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
  8. @auth_bp.route('/login', methods=['POST'])
  9. def login():
  10. info = json.loads(request.data)
  11. username = info.get('username', 'guest')
  12. password = info.get('password', '')
  13. user = Models.User.objects(name=username,
  14. password=password).first()
  15. if user:
  16. login_user(user)
  17. return jsonify(user.to_json())
  18. else:
  19. return jsonify({"status": 401,
  20. "reason": "Username or Password Error"})
  21. @auth_bp.route('/logout', methods=['POST'])
  22. def logout():
  23. logout_user()
  24. return jsonify(**{'result': 200,
  25. 'data': {'message': 'logout success'}})

application/controller/user.py

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. from flask.ext.login import current_user
  4. from flask import Blueprint, jsonify
  5. user_bp = Blueprint('users', __name__, url_prefix='')
  6. @user_bp.route('/user_info', methods=['POST'])
  7. def user_info():
  8. if current_user.is_authenticated:
  9. resp = {"result": 200,
  10. "data": current_user.to_json()}
  11. else:
  12. resp = {"result": 401,
  13. "data": {"message": "user no login"}}
  14. return jsonify(**resp)

application/controller/todo.py

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. import json
  4. from datetime import datetime
  5. from flask import Blueprint, request, jsonify
  6. from flask.ext.login import current_user, login_required
  7. import application.models as Models
  8. todo_bp = Blueprint('todos', __name__, url_prefix='/todo')
  9. @todo_bp.route('/item', methods=['POST'])
  10. @login_required
  11. def create_todo_item():
  12. data = json.loads(request.data)
  13. content = data.get('content')
  14. note = data.get('note', None)
  15. priority = data.get('priority', 0)
  16. if not content:
  17. return jsonify({
  18. 'data': {},
  19. 'msg': 'no content',
  20. 'code': 1001,
  21. 'extra': {}})
  22. item = Models.Item(content=content, created_date=datetime.now(),
  23. completed=False, created_by=current_user.id,
  24. notes=[note] if note else [],
  25. priority=priority)
  26. item.save()
  27. return jsonify({
  28. 'data': item.to_json(),
  29. 'msg': 'create item success',
  30. 'code': 1000,
  31. 'extra': {}
  32. })
  33. @todo_bp.route('/item', methods=['DELETE'])
  34. @login_required
  35. def delete_todo_item():
  36. data = json.loads(request.data)
  37. id = data.get('id')
  38. if not id:
  39. return jsonify({
  40. 'data': {},
  41. 'msg': 'no id',
  42. 'code': 2001,
  43. 'extra': {}})
  44. item = Models.Item.objects(id=id).first()
  45. item.delete()
  46. return jsonify({
  47. 'data': item.to_json(),
  48. 'msg': 'delete item success',
  49. 'code': 2000,
  50. 'extra': {}
  51. })
  52. @todo_bp.route('/item', methods=['PUT'])
  53. @login_required
  54. def update_todo_item():
  55. data = json.loads(request.data)
  56. id = data.get('id')
  57. type = data.get('type')
  58. if type == "update_content":
  59. content = data.get('content')
  60. Models.Item.objects(id=id).first().update(content=content)
  61. elif type == "insert_notes":
  62. note = data.get('note')
  63. Models.Item.objects(id=id).first().update(push__notes=note)
  64. elif type == "done":
  65. Models.Item.objects(id=id).first().update(completed=True,
  66. completed_date=datetime.now())
  67. return jsonify({
  68. 'data': {'oper': type,
  69. 'id': id},
  70. 'msg': 'oper done',
  71. 'code': 3000,
  72. 'extra': {}
  73. })
  74. @todo_bp.route('/item', methods=['GET'])
  75. @login_required
  76. def get_todo_item():
  77. query_string = request.args.get('q')
  78. data = json.loads(query_string)
  79. id = data.get('id')
  80. item = Models.Item.objects(id=id).first()
  81. return jsonify({
  82. 'data': item.to_json(),
  83. 'msg': 'query item success',
  84. 'code': 4000,
  85. 'extra': {}
  86. })
  87. @todo_bp.route('/items', methods=['GET'])
  88. @login_required
  89. def get_todo_items():
  90. data = json.loads(request.args.get('q'))
  91. page = data.get('page', 1)
  92. page_size = data.get('page_size', 10)
  93. begin = (page - 1) * page_size
  94. end = begin + page_size
  95. items = Models.Item.objects()[begin: end]
  96. rsts = []
  97. for item in items:
  98. rsts.append(item.to_json())
  99. return jsonify({
  100. 'data': rsts,
  101. 'msg': 'query items success',
  102. 'code': 5000,
  103. 'extra': {}
  104. })

初始化扩展

扩展我们是统一放到 application/extensions.py 里面进行构建对象的,所以文件有:

application/extensions.py

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. from flask.ext.admin import Admin
  4. from flask.ext.login import LoginManager
  5. from flask.ext.mongoengine import MongoEngine
  6. db = MongoEngine()
  7. login_manager = LoginManager()
  8. admin = Admin()

初始化应用

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. import sys
  4. import logging
  5. from flask import Flask
  6. from flask_admin.contrib.mongoengine import ModelView
  7. from config import load_config
  8. from application.extensions import db, login_manager, admin
  9. from application.models import User, Role
  10. from application.controllers import all_bp
  11. # convert python's encoding to utf8
  12. try:
  13. reload(sys)
  14. sys.setdefaultencoding('utf8')
  15. except (AttributeError, NameError):
  16. pass
  17. def create_app(mode):
  18. """Create Flask app."""
  19. config = load_config(mode)
  20. app = Flask(__name__)
  21. app.config.from_object(config)
  22. if not hasattr(app, 'production'):
  23. app.production = not app.debug and not app.testing
  24. if app.debug or app.testing:
  25. # Log errors to stderr in production mode
  26. app.logger.addHandler(logging.StreamHandler())
  27. app.logger.setLevel(logging.ERROR)
  28. # Register components
  29. register_extensions(app)
  30. register_blueprint(app)
  31. return app
  32. def register_extensions(app):
  33. """Register models."""
  34. db.init_app(app)
  35. login_manager.init_app(app)
  36. # flask-admin configs
  37. admin.init_app(app)
  38. admin.add_view(ModelView(User))
  39. admin.add_view(ModelView(Role))
  40. login_manager.login_view = 'auth.login'
  41. @login_manager.user_loader
  42. def load_user(user_id):
  43. return User.objects(id=user_id).first()
  44. def register_blueprint(app):
  45. for bp in all_bp:
  46. app.register_blueprint(bp)