├── FlaskZhihu
├── __init__.py
├── decorators.py
├── forms
│ ├── __init__.py
│ ├── collection.py
│ ├── answer.py
│ ├── comment.py
│ ├── question.py
│ └── user.py
├── views
│ ├── __init__.py
│ ├── index.py
│ ├── comment.py
│ ├── answer.py
│ ├── question.py
│ ├── collection.py
│ └── user.py
├── constants.py
├── models
│ ├── __init__.py
│ ├── user
│ │ ├── operation
│ │ │ ├── __init__.py
│ │ │ ├── comment.py
│ │ │ ├── collection.py
│ │ │ ├── question.py
│ │ │ ├── user.py
│ │ │ └── answer.py
│ │ └── __init__.py
│ ├── topic.py
│ ├── collection.py
│ ├── base.py
│ ├── comment.py
│ ├── question.py
│ ├── answer.py
│ └── signals.py
├── extensions.py
├── templates
│ ├── question
│ │ ├── index.html
│ │ ├── edit.html
│ │ ├── add.html
│ │ ├── my.html
│ │ └── show.html
│ ├── answer
│ │ ├── edit.html
│ │ ├── my.html
│ │ └── show.html
│ ├── collection
│ │ ├── edit.html
│ │ ├── add.html
│ │ ├── my.html
│ │ ├── select_by_answer.html
│ │ └── show.html
│ ├── user
│ │ ├── edit.html
│ │ ├── login.html
│ │ ├── add.html
│ │ └── _user_banner.html
│ └── base.html
├── permissions.py
├── helpers.py
├── application.py
├── signals.py
├── api
│ └── __init__.py
└── settings.py
├── docs
└── images
│ └── flaskzhihu.png
├── TODO
├── app.py
├── README.md
├── .gitignore
├── requirements.txt
├── scripts
├── import_finish.py
├── import_question.py
├── import_people.py
└── import_answer.py
└── tests
├── test_orm.py
└── orm.py
/FlaskZhihu/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/FlaskZhihu/decorators.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
--------------------------------------------------------------------------------
/docs/images/flaskzhihu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shn7798/FlaskZhihu/HEAD/docs/images/flaskzhihu.png
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | //Done 优化user_on_x 多对多表的速度
2 |
3 | 增加测试用例
4 | 增加后端api接口
5 | 美化前端
6 | 前端动态内容加载, 数据来自后端api
7 | 增加cache优化
8 |
9 | 实现收藏夹
10 | 实现timeline
11 | 实现评论的"查看对话"
12 | 实现通知
13 | 实现搜索
14 | 实现专栏
15 | 实现问题编辑日志
--------------------------------------------------------------------------------
/FlaskZhihu/forms/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from .answer import *
5 | from .comment import *
6 | from .user import *
7 | from .question import *
8 | from .collection import *
--------------------------------------------------------------------------------
/FlaskZhihu/views/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from .answer import *
5 | from .user import *
6 | from .comment import *
7 | from .question import *
8 | from .collection import *
--------------------------------------------------------------------------------
/FlaskZhihu/constants.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | VOTE_UP = 1
5 | VOTE_DOWN = -1
6 | VOTE_NONE = 0
7 |
8 | THANK_ON = 1
9 | THANK_OFF = 0
10 |
11 | FOLLOW_ON = 1
12 | FOLLOW_OFF = 0
--------------------------------------------------------------------------------
/FlaskZhihu/views/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask import redirect
5 | from flask.blueprints import Blueprint
6 |
7 | index = Blueprint('index', __name__)
8 |
9 | @index.route('/')
10 | def index_page():
11 | return redirect('/question/')
--------------------------------------------------------------------------------
/FlaskZhihu/models/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from .answer import *
5 | from .collection import *
6 | from .comment import *
7 | from .question import *
8 | from .topic import *
9 | from .user import *
10 |
11 | # 必须引用, 以使signal修饰器生效
12 | from . import signals
--------------------------------------------------------------------------------
/FlaskZhihu/models/user/operation/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from .answer import AnswerOperationMixin
5 | from .collection import CollectionOperationMixin
6 | from .question import QuestionOperationMixin
7 | from .user import UserOperationMixin
8 | from .comment import CommentOperationMixin
--------------------------------------------------------------------------------
/FlaskZhihu/extensions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask.ext.sqlalchemy import SQLAlchemy
5 | from flask.ext.cache import Cache
6 | from flask.ext.session import Session
7 |
8 | __all__ = ['db', 'cache', 'session']
9 |
10 | db = SQLAlchemy()
11 | cache = Cache()
12 | session = Session()
13 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from FlaskZhihu.application import create_app
3 | from FlaskZhihu.settings import TestSettings, IPythonSettings
4 | if __name__ == '__main__':
5 | settings = IPythonSettings()
6 | app = create_app(settings=settings)
7 | #app.run(host='0.0.0.0', processes=10)
8 | app.run(host='0.0.0.0')
9 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/question/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 随机问题浏览 {% endblock %}
3 | {% block body %}
4 | {% include 'user/_user_banner.html' %}
5 | random_questions:
6 | {% for q in random_questions %}
7 | {{ q.title }} ({{ q.answers_count }})
8 | {% endfor %}
9 |
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FlaskZhihu
2 |
3 | - 这是一个**很水**的web项目
4 |
5 | - 功能完全山寨自知乎
6 |
7 | - 项目结构参考自newsmeme
8 |
9 | - 欢迎PR
10 |
11 | # Quick start
12 | ## 搭建环境
13 | - 安装mysql和redis服务
14 | - 配置virtualenv环境
15 | - 安装依赖包 pip install -r requirements.txt
16 | - 配置settings.py中的IPythonSettings中的参数
17 | - 运行python app.py
18 | - 浏览器打开http://127.0.0.1:5000/
19 |
20 | ## 首页效果图
21 | 
--------------------------------------------------------------------------------
/FlaskZhihu/templates/answer/edit.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 修改回答 {% endblock %}
3 | {% block body %}
4 | {% include 'user/_user_banner.html' %}
5 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/FlaskZhihu/forms/collection.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask.ext.wtf import Form
5 | from wtforms import TextAreaField, SubmitField, HiddenField, StringField
6 | from wtforms.validators import required, Regexp
7 |
8 | class CollectionAddForm(Form):
9 | title = StringField(validators=[required()])
10 | description = StringField(validators=[required()])
11 |
12 |
13 | class CollectionEditForm(Form):
14 | title = StringField()
15 | description = StringField()
--------------------------------------------------------------------------------
/FlaskZhihu/templates/question/edit.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 提问 {% endblock %}
3 | {% block body %}
4 |
19 |
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/collection/edit.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 修改收藏夹 {% endblock %}
3 | {% block body %}
4 |
19 |
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/question/add.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 提问 {% endblock %}
3 | {% block body %}
4 |
19 |
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/user/edit.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 修改 {% endblock %}
3 | {% block body %}
4 |
19 |
20 |
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/FlaskZhihu/models/user/operation/comment.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.constants import VOTE_UP, VOTE_DOWN, VOTE_NONE
5 | class CommentOperationMixin(object):
6 | def voteup_comment(self, comment, undo=False):
7 | op = self.op_on_comment(comment, edit=True)
8 | if undo:
9 | if op.vote == VOTE_UP:
10 | op.vote = VOTE_NONE
11 | else:
12 | op.vote = VOTE_UP
13 |
14 | def add_comment_reply(self, comment, reply):
15 | reply.quote_comment = comment
16 |
17 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/collection/add.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 创建收藏夹 {% endblock %}
3 | {% block body %}
4 |
19 |
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title %}FlaskZhihu{% endblock %}
6 | {% block stylesheets %}
7 | {% endblock %}
8 | {% block extra_stylesheets %}{% endblock %}
9 | {% block favicon %}{% endblock %}
10 | {% block javascripts %}{% endblock %}
11 | {% block extra_javascripts %}{% endblock %}
12 | {% block extrahead %}{% endblock %}
13 |
14 |
15 | {% block body %}
16 | {% endblock %}
17 |
18 |
19 |
--------------------------------------------------------------------------------
/FlaskZhihu/forms/answer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask.ext.wtf import Form
5 | from wtforms import TextAreaField, SubmitField, HiddenField
6 | from wtforms.validators import required, Regexp
7 |
8 | __all__ = ['AnswerAddForm', 'AnswerEditForm']
9 |
10 |
11 | class AnswerAddForm(Form):
12 | question_id = HiddenField(validators=[required(),
13 | Regexp(r'^\d+$')])
14 | content = TextAreaField(validators=[required(message=u'请输入内容')])
15 |
16 |
17 | class AnswerEditForm(Form):
18 | content = TextAreaField(validators=[required(message=u'请输入内容')])
--------------------------------------------------------------------------------
/FlaskZhihu/forms/comment.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask.ext.wtf import Form
5 | from wtforms import TextAreaField, SubmitField, HiddenField
6 | from wtforms.validators import required, Regexp
7 |
8 | __all__ = ['CommentAddForm']
9 |
10 |
11 | class CommentAddForm(Form):
12 | target_type = HiddenField(validators=[required(message=u'请输入评论类型'),
13 | Regexp(r'^[a-z]+$')])
14 | target_id = HiddenField(validators=[required(message=u'请输入评论所属ID'),
15 | Regexp(r'^\d+$')])
16 | content = TextAreaField(validators=[required(message=u'请输入内容')])
--------------------------------------------------------------------------------
/FlaskZhihu/permissions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask.ext.principal import Permission, RoleNeed
5 | from flask.ext.login import LoginManager
6 | from FlaskZhihu.models import User
7 |
8 | admin = Permission(RoleNeed('admin'))
9 | auth = Permission(RoleNeed('authenticated'))
10 |
11 | login_manager = LoginManager()
12 | login_manager.login_view = 'UserView:login'
13 |
14 | @login_manager.user_loader
15 | def user_loader(user_id):
16 | if not user_id:
17 | return None
18 | try:
19 | return User.find_by_id(user_id, abort404=True)
20 | except Exception as e:
21 | print e
22 | return None
--------------------------------------------------------------------------------
/FlaskZhihu/forms/question.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask.ext.wtf import Form
5 | from wtforms import TextAreaField, SubmitField, HiddenField, StringField
6 | from wtforms.validators import required, Regexp
7 |
8 | __all__ = ['QuestionAddForm', 'QuestionEditForm']
9 |
10 |
11 | class QuestionAddForm(Form):
12 | title = StringField(validators=[required(message=u'请输入标题')])
13 | content = TextAreaField(validators=[required(message=u'请输入内容')])
14 |
15 | class QuestionEditForm(Form):
16 | title = StringField(validators=[required(message=u'请输入标题')])
17 | content = TextAreaField(validators=[required(message=u'请输入内容')])
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | settings.py
3 | scripts/import*
4 | # form https://github.com/notedit/flask-base
5 |
6 | *.py[cod]
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Packages
12 | *.egg
13 | *.egg-info
14 | dist
15 | build
16 | eggs
17 | parts
18 | bin
19 | var
20 | sdist
21 | develop-eggs
22 | .installed.cfg
23 | lib
24 | lib64
25 |
26 | # Installer logs
27 | pip-log.txt
28 |
29 | # Unit test / coverage reports
30 | .coverage
31 | .tox
32 | nosetests.xml
33 |
34 | # Translations
35 | *.mo
36 |
37 | # Mr Developer
38 | .mr.developer.cfg
39 | .project
40 | .pydevproject
41 |
42 | # virtualenv
43 | env/
44 |
45 | # tmp files
46 | .DS_Store
47 | *.swp
48 | *.sccsc
49 | *~
50 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/user/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 登录 {% endblock %}
3 | {% block body %}
4 |
21 |
22 |
23 | {% endblock %}
24 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/user/add.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 注册 {% endblock %}
3 | {% block body %}
4 |
23 |
24 |
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/user/_user_banner.html:
--------------------------------------------------------------------------------
1 | {% block user_banner %}
2 | 首页
3 | {% if current_user.is_anonymous %}
4 | |登录
5 | |提问
6 | {% else %}
7 | |欢迎你: {{ current_user.name }}({{ current_user.username }})
8 | |提问
9 | |我的提问
10 | |我的回答
11 | |我的收藏
12 | |修改资料
13 | |退出
14 | {% endif %}
15 |
16 |
17 | {% endblock %}
--------------------------------------------------------------------------------
/FlaskZhihu/models/user/operation/collection.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.constants import *
5 |
6 |
7 | class CollectionOperationMixin(object):
8 | def add_collection_comment(self, collection, comment):
9 | comment.user = self
10 | collection.comments.appen(comment)
11 |
12 | def add_collection_answer(self, collection, answer):
13 | collection.answers.append(answer)
14 |
15 | def follow_collection(self, collection):
16 | op = self.op_on_collection(collection, edit=True)
17 | op.follow = FOLLOW_ON
18 |
19 | def unfollow_collection(self, collection):
20 | op = self.op_on_collection(collection, edit=True)
21 | op.follow = FOLLOW_OFF
--------------------------------------------------------------------------------
/FlaskZhihu/forms/user.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask.ext.wtf import Form
5 | from wtforms import TextAreaField, SubmitField, HiddenField, StringField
6 | from wtforms.validators import required
7 |
8 | __all__ = ['UserLoginForm', 'UserAddForm', 'UserEditForm']
9 |
10 | class UserLoginForm(Form):
11 | username = StringField(validators=[required()])
12 | password = StringField(validators=[required()])
13 | next_url = HiddenField()
14 |
15 |
16 | class UserAddForm(Form):
17 | name = StringField(validators=[required()])
18 | username = StringField(validators=[required()])
19 | password = StringField(validators=[required()])
20 |
21 |
22 | class UserEditForm(Form):
23 | name = StringField(validators=[])
24 | password = StringField(validators=[])
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | appnope==0.1.0
2 | backports.shutil-get-terminal-size==1.0.0
3 | blinker==1.4
4 | decorator==4.0.9
5 | Flask==0.10.1
6 | Flask-Cache==0.13.1
7 | Flask-Classy==0.6.10
8 | Flask-Login==0.3.2
9 | Flask-Principal==0.4.0
10 | Flask-Session==0.2.3
11 | Flask-SQLAlchemy==2.1
12 | Flask-SQLAlchemy-Cache==0.1.5
13 | Flask-Testing==0.4.2
14 | Flask-WTF==0.12
15 | gnureadline==6.3.3
16 | inflect==0.2.5
17 | ipython==4.2.0
18 | ipython-genutils==0.1.0
19 | itsdangerous==0.24
20 | Jinja2==2.8
21 | MarkupSafe==0.23
22 | MySQL-python==1.2.5
23 | pathlib2==2.1.0
24 | pexpect==4.0.1
25 | pickleshare==0.7.2
26 | ptyprocess==0.5.1
27 | pymongo==3.2.2
28 | redis==2.10.5
29 | simplegeneric==0.8.1
30 | six==1.10.0
31 | sqlacodegen==1.1.6
32 | SQLAlchemy==1.0.12
33 | traitlets==4.2.1
34 | Werkzeug==0.11.9
35 | WTForms==2.1
36 |
--------------------------------------------------------------------------------
/scripts/import_finish.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from pymongo import MongoClient
5 |
6 | from flask import Flask
7 |
8 | from FlaskZhihu.settings import IPythonSettings
9 | from FlaskZhihu.models import *
10 | from FlaskZhihu.extensions import db
11 |
12 | def enc(s):
13 | if isinstance(s, unicode):
14 | return s.encode('UTF-8')
15 | else:
16 | return s
17 |
18 | app = Flask(__name__)
19 | app.config.from_object(IPythonSettings())
20 | db.init_app(app)
21 |
22 | ctx = app.app_context()
23 |
24 | ctx.push()
25 | mysql = db.session()
26 |
27 | update_answers_count_sql = """UPDATE question q SET answers_count = (SELECT COUNT(1) FROM answer a WHERE a.question_id = q.id) WHERE answers_count <= 0"""
28 |
29 |
30 | mysql.execute(update_answers_count_sql)
31 | mysql.commit()
--------------------------------------------------------------------------------
/FlaskZhihu/templates/question/my.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 我的提问 {% endblock %}
3 | {% block body %}
4 | {% include 'user/_user_banner.html' %}
5 |
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/FlaskZhihu/models/user/operation/question.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.constants import *
5 |
6 |
7 | class QuestionOperationMixin(object):
8 | def add_question(self, question):
9 | """ 创建问题 """
10 | question.user = self
11 |
12 | def add_question_answer(self, question, answer):
13 | """ 回答问题 """
14 | answer.user = self
15 | question.answers.append(answer)
16 |
17 | def add_question_comment(self, question, comment):
18 | """ 评论问题 """
19 | comment.user = self
20 | question.comments.append(comment)
21 |
22 | def follow_question(self, question):
23 | """ 关注问题 """
24 | op = self.op_on_question(question, edit=True)
25 | op.follow = FOLLOW_ON
26 |
27 | def unfollow_question(self, question):
28 | """ 取消关注问题 """
29 | op = self.op_on_question(question, edit=True)
30 | op.follow = FOLLOW_OFF
--------------------------------------------------------------------------------
/FlaskZhihu/templates/answer/my.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 我的回答 {% endblock %}
3 | {% block body %}
4 | {% include 'user/_user_banner.html' %}
5 |
29 |
30 | {% endblock %}
31 |
--------------------------------------------------------------------------------
/FlaskZhihu/helpers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask import session, request
5 | import blinker
6 | from FlaskZhihu.extensions import cache
7 | import functools
8 |
9 | def cached(*cargs, **ckwargs):
10 | def decorator(func):
11 | @functools.wraps(func)
12 | def wrapper(*args, **kwargs):
13 | return cache.cached(*cargs, unless=lambda: session.get('user_id', None) is not None, **ckwargs)(func)(*args, **kwargs)
14 | return wrapper
15 | return decorator
16 |
17 |
18 | def keep_next_url(func):
19 | @functools.wraps(func)
20 | def wrapper(*args, **kwargs):
21 | next_url = request.args.get('next')
22 | if not next_url:
23 | if request and request.method in ("PUT", "POST"):
24 | next_url = request.form.get('next')
25 | return func(*args, next_url=next_url, **kwargs)
26 | return wrapper
27 |
28 |
29 | def use_signal(signal):
30 | assert isinstance(signal, blinker.NamedSignal)
31 |
32 | def decorator(func):
33 | signal.connect(func)
34 | return func
35 | return decorator
36 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/collection/my.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 我的收藏夹 {% endblock %}
3 | {% block body %}
4 | {% include 'user/_user_banner.html' %}
5 | 新建
6 |
7 |
8 |
9 |
10 | | 创建时间 |
11 | 收藏夹名称 |
12 | 收藏的答案 |
13 | 关注人数 |
14 | 编辑 |
15 | 删除 |
16 |
17 |
18 |
19 | {% for collection in collections %}
20 |
21 | | {{ collection.create_time }} |
22 | {{ collection.title }} |
23 | 收藏的答案({{ collection.answers_count }}) |
24 | {{ collection.following_count }} |
25 | 编辑 |
26 | 删除 |
27 |
28 | {% endfor %}
29 |
30 |
31 |
32 | {% endblock %}
33 |
--------------------------------------------------------------------------------
/FlaskZhihu/models/user/operation/user.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 |
5 | class UserOperationMixin(object):
6 | def follow_user(self, user):
7 | op = self.op_on_user(user, edit=True)
8 | op.follow = True
9 |
10 | def unfollow_user(self, user):
11 | op = self.op_on_user(user, edit=True)
12 | op.follow = False
13 |
14 | def block_user(self, user):
15 | op = self.op_on_user(user, edit=True)
16 | op.follow = False
17 | op.block = True
18 |
19 | def unblock_user(self, user):
20 | op = self.op_on_user(user, edit=True)
21 | op.block = False
22 |
23 | @property
24 | def followed_users(self):
25 | return [op.dest_user for op in self.user_on_dest_user if op.follow]
26 |
27 | @property
28 | def who_followed_me(self):
29 | return [op.user for op in self.dest_user_on_user if op.follow]
30 |
31 | @property
32 | def blocked_users(self):
33 | return [op.dest_user for op in self.user_on_dest_user if op.block]
34 |
35 | @property
36 | def who_blocked_me(self):
37 | return [op.user for op in self.dest_user_on_user if op.block]
38 |
39 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/collection/select_by_answer.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 选择收藏夹 {% endblock %}
3 | {% block body %}
4 | {% include 'user/_user_banner.html' %}
5 | 新建
6 |
7 |
8 |
9 |
10 | | 创建时间 |
11 | 收藏夹名称 |
12 | 编辑 |
13 | 收藏的答案 |
14 | 关注人数 |
15 | 操作 |
16 |
17 |
18 |
19 | {% for collection in collections %}
20 |
21 | | {{ collection.create_time }} |
22 | {{ collection.title }} |
23 | 编辑 |
24 | 收藏的答案({{ collection.answers_count }}) |
25 | {{ collection.following_count }} |
26 | 选择 |
27 |
28 | {% endfor %}
29 |
30 |
31 |
32 | {% endblock %}
33 |
--------------------------------------------------------------------------------
/FlaskZhihu/models/user/operation/answer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.constants import VOTE_NONE, VOTE_UP, VOTE_DOWN, THANK_OFF, THANK_ON
5 |
6 | class AnswerOperationMixin(object):
7 | def add_answer(self, answer):
8 | """ 创建回答 """
9 | answer.user = self
10 |
11 | def add_answer_comment(self, answer, comment):
12 | """ 评论回答 """
13 | comment.user = self
14 | answer.comments.append(comment)
15 |
16 | def voteup_answer(self, answer, undo=False):
17 | op = self.op_on_answer(answer, edit=True)
18 | if undo:
19 | if op.vote == VOTE_UP:
20 | op.vote = VOTE_NONE
21 | else:
22 | op.vote = VOTE_UP
23 |
24 |
25 | def votedown_answer(self, answer, undo=False):
26 | op = self.op_on_answer(answer, edit=True)
27 | if undo:
28 | if op.vote == VOTE_DOWN:
29 | op.vote = VOTE_NONE
30 | else:
31 | op.vote = VOTE_DOWN
32 |
33 | def thank_answer(self, answer):
34 | op = self.op_on_answer(answer, edit=True)
35 | op.thank = THANK_ON
36 |
37 | def unthank_answer(self, answer):
38 | op = self.op_on_answer(answer, edit=True)
39 | op.thank = THANK_OFF
--------------------------------------------------------------------------------
/FlaskZhihu/application.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from flask import Flask
3 |
4 | from FlaskZhihu.settings import DefaultSettings
5 | from FlaskZhihu.extensions import db, cache, session
6 | from FlaskZhihu.views.index import index
7 | from FlaskZhihu.views import QuestionView, AnswerView, CommentView, UserView, CollectionView
8 | from FlaskZhihu.permissions import login_manager
9 | from FlaskZhihu.api import *
10 |
11 |
12 | def create_app(settings=None):
13 | if not settings:
14 | settings = DefaultSettings()
15 | app = Flask(__name__)
16 | app.config.from_object(settings)
17 |
18 | # flask login
19 | login_manager.init_app(app)
20 | session.init_app(app)
21 |
22 | init_extensions(app)
23 | init_views(app)
24 | return app
25 |
26 |
27 | def init_extensions(app):
28 | db.init_app(app)
29 | cache.init_app(app)
30 |
31 |
32 | def init_views(app):
33 | app.register_blueprint(index)
34 | QuestionView.register(app)
35 | AnswerView.register(app)
36 | CommentView.register(app)
37 | UserView.register(app)
38 | CollectionView.register(app)
39 |
40 | # api
41 | QuestionApiView.register(app)
42 | AnswerApiView.register(app)
43 | UserApiView.register(app)
44 | CommentApiView.register(app)
45 | CollectionApiView.register(app)
--------------------------------------------------------------------------------
/FlaskZhihu/models/topic.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.extensions import db
5 | from FlaskZhihu.models.base import DateTimeMixin, FindByIdMixin
6 | from flask.ext.sqlalchemy_cache import CachingQuery
7 |
8 |
9 | class Topic(DateTimeMixin, FindByIdMixin, db.Model):
10 | __tablename__ = 'topic'
11 | query_class = CachingQuery
12 |
13 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
14 | name = db.Column('name', db.String(60))
15 | description = db.Column('description', db.String(1024))
16 | avatar_url = db.Column('avatar_url', db.String(200))
17 |
18 | questions = db.relationship(u'Question', secondary='topic_and_question', backref='topics')
19 | user_on_topic = db.relationship(u'UserOnTopic', backref='topic')
20 |
21 | def __repr__(self):
22 | return '<%s %r>' % (self.__class__.__name__, self.name)
23 |
24 |
25 |
26 | class TopicAndQuestion(DateTimeMixin, FindByIdMixin, db.Model):
27 | __tablename__ = 'topic_and_question'
28 |
29 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
30 | topic_id = db.Column('topic_id', db.ForeignKey(u'topic.id'), nullable=False, index=True)
31 | question_id = db.Column('question_id', db.ForeignKey(u'question.id'), nullable=False, index=True)
32 |
--------------------------------------------------------------------------------
/scripts/import_question.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from pymongo import MongoClient
5 |
6 | from flask import Flask
7 |
8 | from FlaskZhihu.settings import IPythonSettings
9 | from FlaskZhihu.models import *
10 | from FlaskZhihu.extensions import db
11 |
12 | def enc(s):
13 | if isinstance(s, unicode):
14 | return s.encode('UTF-8')
15 | else:
16 | return s
17 |
18 | app = Flask(__name__)
19 | app.config.from_object(IPythonSettings())
20 | db.init_app(app)
21 |
22 | ctx = app.app_context()
23 |
24 | ctx.push()
25 | mysql = db.session()
26 |
27 | mg = MongoClient('192.168.5.202').zhihu
28 |
29 | print Question.query.delete()
30 | mysql.commit()
31 | #exit(1)
32 |
33 | cur = mg.questions.find() #.limit(100000)
34 | i = 0
35 | for item in cur:
36 | try:
37 | d = item['data']
38 | o = Question()
39 | o.title = enc(d['title'])
40 | o.id = int(d['id'])
41 | o.excerpt = enc(d.get('excerpt', ''))
42 | o.content = enc(d['detail'])
43 | o.user_hashid = enc(d['author']['id'])
44 | # o.answers_count = Answer.query.filter(Answer.question_id == o.id).count()
45 |
46 | u = User.get_user_by_hashid(o.user_hashid)
47 | if u:
48 | o.user = u
49 |
50 | mysql.add(o)
51 | except:
52 | pass
53 |
54 | i += 1
55 | if i % 100 == 0:
56 | mysql.commit()
57 |
58 | mysql.commit()
--------------------------------------------------------------------------------
/scripts/import_people.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from pymongo import MongoClient
5 |
6 | from flask import Flask
7 |
8 | from FlaskZhihu.settings import IPythonSettings
9 | from FlaskZhihu.models import *
10 | from FlaskZhihu.extensions import db
11 |
12 | def enc(s):
13 | return s.encode('UTF-8')
14 |
15 | app = Flask(__name__)
16 | app.config.from_object(IPythonSettings())
17 | db.init_app(app)
18 |
19 | ctx = app.app_context()
20 |
21 | ctx.push()
22 | mysql = db.session()
23 |
24 | mg = MongoClient('192.168.5.202').zhihu
25 |
26 | print User.query.delete()
27 | mysql.commit()
28 | #exit(1)
29 | cur = mg.people.find() #.limit(100000)
30 |
31 | i = 0
32 | for people in cur:
33 | try:
34 | p = people['data']
35 |
36 |
37 | u = User()
38 | u.hashid = p['id']
39 | u.name = enc(p['name'])
40 | u.password = '123456'
41 | if User.query.filter(User.username == enc(p['name'])).count():
42 | u.username = enc(u'%s_%s' % (p['name'] ,u.hashid))
43 | else:
44 | u.username = enc(p['name'])
45 | u.avatar_url = enc(p['avatar_url'])
46 | u.headline = enc(p['headline'])
47 | u.description = enc(p['description'])
48 | u.user_hashid = p['id']
49 | u.gender = p['gender']
50 |
51 | mysql.add(u)
52 | except:
53 | pass
54 |
55 | i += 1
56 | if i % 100 == 0:
57 | mysql.commit()
58 |
59 | mysql.commit()
--------------------------------------------------------------------------------
/FlaskZhihu/templates/collection/show.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} {{ collection.title }} {% endblock %}
3 | {% block body %}
4 | {% include 'user/_user_banner.html' %}
5 | title: {{ collection.title }}
6 | 编辑
7 | |关注
8 | |取消关注
9 |
10 | description: {{ collection.description }}
11 | answers:
12 |
13 |
14 | {% for answer in collection.answers %}
15 |
16 | | 移除 |
17 | 赞同({{ answer.voteup_count }}) |
18 | 反对({{ answer.votedown_count }}) |
19 | 取消意见 |
20 |
21 | 详情 |
22 | {% if answer.user %}
23 | {{ answer.user.name }} {{ answer.user.hashid }} |
24 | {% else %}
25 | 未知 |
26 | {% endif %}
27 | {{ answer.content|safe }} |
28 |
29 | {% endfor %}
30 |
31 |
32 |
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/scripts/import_answer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from pymongo import MongoClient
5 |
6 | from flask import Flask
7 |
8 | from FlaskZhihu.settings import IPythonSettings
9 | from FlaskZhihu.models import *
10 | from FlaskZhihu.extensions import db
11 | import datetime
12 | def enc(s):
13 | if isinstance(s, unicode):
14 | return s.encode('UTF-8')
15 | else:
16 | return s
17 |
18 | app = Flask(__name__)
19 | app.config.from_object(IPythonSettings())
20 | db.init_app(app)
21 |
22 | ctx = app.app_context()
23 |
24 | ctx.push()
25 | mysql = db.session()
26 |
27 | mg = MongoClient('192.168.5.202').zhihu
28 |
29 | print Answer.query.delete()
30 | mysql.commit()
31 | #exit(1)
32 |
33 | cur = mg.answers.find() #.limit(100000)
34 |
35 | i = 0
36 | for item in cur:
37 | try:
38 | d = item['data']
39 | o = Answer()
40 | o.id = d['id']
41 | o.content = enc(d['content'])
42 | o.excerpt = enc(d.get('excerpt', ''))
43 | create_time = d.get('created_time', None)
44 | if create_time:
45 | o.create_time = datetime.datetime.fromtimestamp(create_time)
46 | update_time = d.get('updated_time', None)
47 | if update_time:
48 | o.update_time = datetime.datetime.fromtimestamp(update_time)
49 |
50 | o.user_hashid = enc(d['author']['id'])
51 | u = User.get_user_by_hashid(o.user_hashid)
52 | if u:
53 | o.user = u
54 |
55 | o.question_id = int(d['question']['id'])
56 |
57 | mysql.add(o)
58 | except:
59 | pass
60 |
61 | i += 1
62 | if i % 100 == 0:
63 | mysql.commit()
64 |
65 | mysql.commit()
--------------------------------------------------------------------------------
/FlaskZhihu/models/collection.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.extensions import db
5 | from FlaskZhihu.models.base import DateTimeMixin, FindByIdMixin
6 | from flask.ext.sqlalchemy_cache import CachingQuery
7 |
8 |
9 | class Collection(DateTimeMixin, FindByIdMixin, db.Model):
10 | __tablename__ = 'collection'
11 | query_class = CachingQuery
12 |
13 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
14 | title = db.Column('title', db.String(60))
15 | description = db.Column('description', db.String(4096))
16 | user_id = db.Column('user_id', db.ForeignKey(u'user.id'), nullable=False, index=True)
17 | user_hashid = db.Column('user_hashid', db.String(32))
18 |
19 | answers_count = db.Column('answers_count', db.Integer, server_default='0')
20 | following_count = db.Column('following_count', db.Integer, server_default='0')
21 |
22 | answers = db.relationship(u'Answer', secondary='collection_and_answer', backref='collections')
23 | comments = db.relationship(u'Comment', backref='collection')
24 | user_on_collection = db.relationship(u'UserOnCollection', backref='collection')
25 |
26 | def __repr__(self):
27 | return '<%s %r>' % (self.__class__.__name__, self.title)
28 |
29 |
30 | class CollectionAndAnswer(DateTimeMixin, FindByIdMixin, db.Model):
31 | __tablename__ = 'collection_and_answer'
32 |
33 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
34 | collection_id = db.Column('collection_id', db.ForeignKey(u'collection.id'), nullable=False, index=True)
35 | answer_id = db.Column('answer_id', db.ForeignKey(u'answer.id'), nullable=False, index=True)
36 |
--------------------------------------------------------------------------------
/FlaskZhihu/templates/question/show.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} {{ question.title }} {% endblock %}
3 | {% block body %}
4 | {% include 'user/_user_banner.html' %}
5 | title: {{ question.title }}
6 |
7 | content: {{ question.content|safe }}
8 | answers:
9 |
28 |
29 |
34 |
35 | random_questions:
36 | {% for q in random_questions %}
37 | {{ q.title }} ({{ q.answers_count }})
38 | {% endfor %}
39 |
40 |
41 | {% endblock %}
42 |
--------------------------------------------------------------------------------
/FlaskZhihu/models/base.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.extensions import db
5 | import datetime
6 |
7 | class FindByIdMixin():
8 | @classmethod
9 | def find_by_id(cls, id, abort404=False, pk='id'):
10 | pk_col = getattr(cls, pk)
11 | query = cls.query.filter(pk_col==id)
12 | if abort404:
13 | return query.first_or_404()
14 | else:
15 | return query.first()
16 |
17 | class DateTimeMixin():
18 | create_time = db.Column('create_time', db.DateTime, default=datetime.datetime.now)
19 | update_time = db.Column('update_time', db.DateTime,
20 | default=datetime.datetime.now,
21 | onupdate=datetime.datetime.now)
22 |
23 |
24 | def try_decode(s, enc_list=['UTF-8', 'GBK']):
25 | for enc in enc_list:
26 | try:
27 | v = s.decode(enc)
28 | except:
29 | pass
30 | else:
31 | return v
32 | return s
33 |
34 | def try_encode(s, enc_list=['UTF-8', 'GBK']):
35 | for enc in enc_list:
36 | try:
37 | v = s.encode(enc)
38 | except:
39 | pass
40 | else:
41 | return v
42 | return s
43 |
44 | def blob_unicode(key_name):
45 | def _set_blob(self, val):
46 | if isinstance(val, unicode):
47 | val = try_encode(val)
48 | setattr(self, key_name, val)
49 |
50 |
51 | def _get_blob(self):
52 | val = getattr(self, key_name, None)
53 | if val is None:
54 | return val
55 |
56 | if isinstance(val, unicode):
57 | return val
58 | else:
59 | return try_decode(val)
60 |
61 | return db.synonym(key_name, descriptor=property(_get_blob, _set_blob))
62 |
--------------------------------------------------------------------------------
/FlaskZhihu/signals.py:
--------------------------------------------------------------------------------
1 | from blinker import Namespace
2 |
3 | signals = Namespace()
4 | #
5 | # signals_names = [
6 | # 'question_answer_add',
7 | # 'question_follow',
8 | # 'question_unfollow',
9 | # 'question_comment_add',
10 | # 'question_comment_delete',
11 | # 'answer_comment_add',
12 | # 'answer_comment_delete',
13 | # 'answer_voteup',
14 | # 'answer_votedown',
15 | # 'answer_cancel_vote',
16 | # ]
17 | #
18 | # signals_defines = dict()
19 | # for name in signals_names:
20 | # print "%s = signals.signal('%s')" % (name, name)
21 | # signals_defines[name] = signals.signal(name)
22 | #
23 | # globals().update(signals_defines)
24 | #
25 | # __all__ = signals_names
26 |
27 | question_answer_add = signals.signal('question_answer_add')
28 | question_follow = signals.signal('question_follow')
29 | question_unfollow = signals.signal('question_unfollow')
30 | question_comment_add = signals.signal('question_comment_add')
31 | question_comment_delete = signals.signal('question_comment_delete')
32 | answer_comment_add = signals.signal('answer_comment_add')
33 | answer_comment_delete = signals.signal('answer_comment_delete')
34 | answer_voteup = signals.signal('answer_voteup')
35 | answer_votedown = signals.signal('answer_votedown')
36 | answer_cancel_vote = signals.signal('answer_cancel_vote')
37 | answer_thank = signals.signal('answer_thank')
38 | answer_unthank = signals.signal('answer_unthank')
39 | comment_voteup = signals.signal('comment_voteup')
40 | comment_cancel_vote = signals.signal('comment_cancel_vote')
41 | collection_answer_add = signals.signal('collection_answer_add')
42 | collection_answer_delete = signals.signal('collection_answer_delete')
43 | collection_follow = signals.signal('collection_follow')
44 | collection_unfollow = signals.signal('collection_unfollow')
45 |
--------------------------------------------------------------------------------
/FlaskZhihu/api/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask import url_for, abort, redirect, render_template, request
5 | from flask.ext.classy import FlaskView, route
6 | from flask.ext.login import current_user, login_required
7 |
8 | from FlaskZhihu.extensions import db
9 | from FlaskZhihu.forms import AnswerAddForm
10 | from FlaskZhihu.models import Answer, Question, User
11 | from FlaskZhihu.signals import *
12 |
13 | __all__ = ['AnswerApiView', 'QuestionApiView', 'UserApiView', 'CollectionApiView', 'CommentApiView']
14 |
15 | class BaseApiView(FlaskView):
16 | route_prefix = '/api'
17 |
18 |
19 | class QuestionApiView(BaseApiView):
20 | route_base = '/question'
21 | decorators = [login_required]
22 |
23 | @route('/follow//')
24 | def follow(self, id):
25 | pass
26 |
27 | @route('/collect//')
28 | def collect(self, id):
29 | pass
30 |
31 |
32 | class AnswerApiView(BaseApiView):
33 | route_base = '/answer'
34 | decorators = [login_required]
35 |
36 | @route('/vote//')
37 | def vote(self, id):
38 | pass
39 |
40 | @route('/follow//')
41 | def follow(self, id):
42 | pass
43 |
44 | @route('/comment//')
45 | def comment(self, id):
46 | pass
47 |
48 | @route('/thank//')
49 | def thank(self, id):
50 | pass
51 |
52 |
53 | class UserApiView(BaseApiView):
54 | route_base = '/user'
55 | decorators = [login_required]
56 |
57 | def add(self):
58 | pass
59 |
60 |
61 | class CommentApiView(BaseApiView):
62 | route_base = '/comment'
63 | decorators = [login_required]
64 |
65 |
66 |
67 | class CollectionApiView(BaseApiView):
68 | route_base = '/collection'
69 | decorators = [login_required]
--------------------------------------------------------------------------------
/FlaskZhihu/templates/answer/show.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} {{ answer.user.name }} 的回答 {% endblock %}
3 | {% block body %}
4 | {% include 'user/_user_banner.html' %}
5 | 作者: {{ answer.user.name }}
6 |
收藏
7 | |赞同({{ answer.voteup_count }})
8 | |反对({{ answer.votedown_count }})
9 | |取消意见
10 | |感谢({{ answer.thanks_count }})
11 | |取消感谢
12 |
13 | content: {{ answer.content|safe }}
14 |
15 |
16 |
22 |
23 |
24 |
25 |
26 | {% for comment in answer.comments %}
27 |
28 | | 赞同({{ comment.voteup_count }}) |
29 | 取消意见 |
30 | {{ comment.create_time }} |
31 | {{ comment.user.name }} |
32 | {{ comment.content }} |
33 |
34 |
35 | {% endfor %}
36 |
37 |
38 |
39 | {% endblock %}
40 |
--------------------------------------------------------------------------------
/FlaskZhihu/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 | from redis import Redis
4 |
5 |
6 | class DefaultSettings(object):
7 | DEBUG = True
8 | SQLALCHEMY_DATABASE_URI = "mysql://flaskzhihu:123456@192.168.5.202/flaskzhihu?charset=UTF-8"
9 | SQLALCHEMY_ECHO = False
10 | SQLALCHEMY_TRACK_MODIFICATIONS = True
11 | SECRET_KEY = "asasasasas"
12 |
13 |
14 | class TestSettings(object):
15 | DEBUG = True
16 | SQLALCHEMY_DATABASE_URI = "mysql://flaskzhihu:123456@192.168.5.202/flaskzhihu_test?charset=utf8"
17 | SQLALCHEMY_ECHO = False
18 | SQLALCHEMY_TRACK_MODIFICATIONS = True
19 | WTF_CSRF_ENABLED = False
20 | SECRET_KEY = "asasasasas"
21 | CACHE_TYPE = "simple"
22 | CACHE_DEFAULT_TIMEOUT = 60
23 | SQLALCHEMY_RECORD_QUERIES = True
24 |
25 |
26 | class IPythonSettings(object):
27 | DEBUG = True
28 | WTF_CSRF_ENABLED = False
29 | SECRET_KEY = "asasasasas"
30 |
31 | ###### flask-sqlalchemy #########
32 | SQLALCHEMY_DATABASE_URI = "mysql://flaskzhihu:123456@192.168.5.202/flaskzhihu_test?charset=utf8"
33 | SQLALCHEMY_ECHO = False
34 | SQLALCHEMY_TRACK_MODIFICATIONS = True
35 | SQLALCHEMY_RECORD_QUERIES = True
36 |
37 | ####### flask-cache ###########
38 | CACHE_TYPE = 'redis'
39 | CACHE_REDIS_HOST = '127.0.0.1'
40 | CACHE_KEY_PREFIX = 'flask_cache_'
41 | CACHE_REDIS_DB = 1
42 | #CACHE_TYPE = "simple"
43 | CACHE_DEFAULT_TIMEOUT = 600
44 |
45 | ######## flask-session ########
46 | SESSION_TYPE = 'redis'
47 | SESSION_KEY_PREFIX = 'flask_session_'
48 | # SESSION_REDIS = Redis('127.0.0.1', db=0)
49 | SESSION_REDIS = property(lambda self: Redis('127.0.0.1', db=1))
50 |
51 |
52 | class DeploySettings(IPythonSettings):
53 | SQLALCHEMY_DATABASE_URI = "mysql://flaskzhihu:123456@192.168.5.202/flaskzhihu?charset=utf8"
54 |
55 |
--------------------------------------------------------------------------------
/FlaskZhihu/models/comment.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.models.user import UserOnComment
5 | from FlaskZhihu.extensions import db
6 | from FlaskZhihu.models.base import DateTimeMixin, FindByIdMixin
7 | from FlaskZhihu.constants import VOTE_UP, VOTE_NONE, VOTE_DOWN
8 | from flask.ext.sqlalchemy_cache import CachingQuery
9 |
10 |
11 | class Comment(DateTimeMixin, FindByIdMixin, db.Model):
12 | __tablename__ = 'comment'
13 | query_class = CachingQuery
14 |
15 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
16 | content = db.Column('content', db.String(4096))
17 | comment_target = db.Column('comment_target', db.String(10))
18 | user_id = db.Column('user_id', db.ForeignKey(u'user.id'), index=True)
19 | user_hashid = db.Column('user_hashid', db.String(32))
20 |
21 | quote_comment_id = db.Column('quote_comment_id', db.ForeignKey(u'comment.id'), index=True)
22 | answer_id = db.Column('answer_id', db.ForeignKey(u'answer.id'), index=True)
23 | question_id = db.Column('question_id', db.ForeignKey(u'question.id'), index=True)
24 | collection_id = db.Column('collection_id', db.ForeignKey(u'collection.id'), index=True)
25 |
26 | user_on_comment = db.relationship(u'UserOnComment', backref='comment')
27 | reply_comments = db.relationship("Comment",
28 | backref=db.backref('quote_comment', remote_side=[id]))
29 |
30 | # count cache
31 | voteup_count = db.Column('voteup_count', db.Integer, server_default='0')
32 |
33 | @property
34 | def voteup_users(self):
35 | ops = UserOnComment.query.filter(
36 | db.and_(
37 | UserOnComment.comment_id == self.id,
38 | UserOnComment.vote == VOTE_UP,
39 | )
40 | ).all()
41 |
42 | return [op.user for op in ops]
43 |
44 | def __repr__(self):
45 | return '<%s %r>' % (self.__class__.__name__, self.content[0:8])
--------------------------------------------------------------------------------
/FlaskZhihu/models/question.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.extensions import db
5 | from FlaskZhihu.models.user import UserOnQuestion
6 | from FlaskZhihu.models.base import DateTimeMixin, FindByIdMixin, blob_unicode
7 | from flask.ext.sqlalchemy_cache import CachingQuery
8 |
9 |
10 | class Question(DateTimeMixin, FindByIdMixin, db.Model):
11 | __tablename__ = 'question'
12 | query_class = CachingQuery
13 |
14 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
15 | answers_update_time = db.Column('answers_update_time', db.DateTime)
16 | title = db.Column('title', db.String(500))
17 | excerpt = db.Column('excerpt', db.String(4096))
18 | answer_ids = db.Column('answer_ids', db.Integer)
19 |
20 | # count cache
21 | voteup_count = db.Column('voteup_count', db.Integer, server_default='0')
22 | votedown_count = db.Column('votedown_count', db.Integer, server_default='0')
23 | answers_count = db.Column('answers_count', db.Integer, server_default='0')
24 | following_count = db.Column('following_count', db.Integer, server_default='0')
25 | comments_count = db.Column('comments_count', db.Integer, server_default='0')
26 |
27 | status = db.Column('status', db.String(45))
28 | user_id = db.Column('user_id', db.ForeignKey(u'user.id'), index=True)
29 | user_hashid = db.Column('user_hashid', db.String(32))
30 |
31 | answers = db.relationship(u'Answer', backref='question')
32 | comments = db.relationship(u'Comment', backref='question')
33 |
34 | _content = db.Column('content', db.LargeBinary)
35 | content = blob_unicode('_content')
36 |
37 | user_on_question = db.relationship(u'UserOnQuestion', backref='question')
38 |
39 | @property
40 | def following_users(self):
41 | uoqs = UserOnQuestion.query.filter(
42 | db.and_(UserOnQuestion.question_id == self.id, UserOnQuestion.follow==True)
43 | ).all()
44 |
45 | return [uoq.user for uoq in uoqs]
46 |
47 |
48 | def __repr__(self):
49 | return '<%s %r>' % (self.__class__.__name__, self.title)
50 |
--------------------------------------------------------------------------------
/FlaskZhihu/models/answer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.extensions import db
5 | from FlaskZhihu.models.base import DateTimeMixin, FindByIdMixin, blob_unicode
6 | from FlaskZhihu.models.user import UserOnAnswer
7 | from FlaskZhihu.constants import VOTE_UP, VOTE_DOWN, VOTE_NONE, THANK_ON
8 | from flask.ext.sqlalchemy_cache import CachingQuery
9 |
10 |
11 | class Answer(DateTimeMixin, FindByIdMixin, db.Model):
12 | __tablename__ = 'answer'
13 | query_class = CachingQuery
14 |
15 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
16 | thanks_count = db.Column('thanks_count', db.Integer)
17 | excerpt = db.Column('excerpt', db.String(4096))
18 | user_id = db.Column('user_id', db.ForeignKey(u'user.id'), index=True)
19 | user_hashid = db.Column('user_hashid', db.String(32))
20 | question_id = db.Column('question_id', db.ForeignKey(u'question.id'), index=True)
21 | collection_id = db.Column('collection_id', db.ForeignKey(u'collection.id'), index=True)
22 |
23 | user_on_answer = db.relationship(u'UserOnAnswer', backref='answer')
24 | comments = db.relationship(u'Comment', backref='answer')
25 |
26 | _content = db.Column('content', db.LargeBinary)
27 | content = blob_unicode('_content')
28 |
29 | # count cache
30 | comments_count = db.Column('comments_count', db.Integer, server_default='0')
31 | voteup_count = db.Column('voteup_count', db.Integer, server_default='0')
32 | votedown_count = db.Column('votedown_count', db.Integer, server_default='0')
33 |
34 |
35 |
36 | @property
37 | def vote_count(self):
38 | return self.voteup_count - self.votedown_count
39 |
40 | @property
41 | def voteup_users(self):
42 | ops = UserOnAnswer.query.filter(
43 | db.and_(
44 | UserOnAnswer.answer_id==self.id,
45 | UserOnAnswer.vote==VOTE_UP,
46 | )
47 | ).all()
48 |
49 | return [op.user for op in ops]
50 |
51 | @property
52 | def votedown_users(self):
53 | ops = UserOnAnswer.query.filter(
54 | db.and_(
55 | UserOnAnswer.answer_id == self.id,
56 | UserOnAnswer.vote == VOTE_DOWN,
57 | )
58 | ).all()
59 |
60 | return [op.user for op in ops]
61 |
62 | @property
63 | def thanked_users(self):
64 | ops = UserOnAnswer.query.filter(
65 | db.and_(
66 | UserOnAnswer.answer_id == self.id,
67 | UserOnAnswer.thank == THANK_ON,
68 | )
69 | ).all()
70 | return [op.user for op in ops]
71 |
72 |
73 | def __repr__(self):
74 | return '<%s %r>' % (self.__class__.__name__, self.id)
--------------------------------------------------------------------------------
/FlaskZhihu/views/comment.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 | from flask import url_for, abort, redirect, render_template
4 | from flask.ext.classy import FlaskView, route, request
5 | from flask.ext.login import current_user, login_required
6 |
7 | from FlaskZhihu.extensions import db
8 | from FlaskZhihu.forms import CommentAddForm
9 | from FlaskZhihu.models import Answer, Question, Comment, Collection, User
10 | from FlaskZhihu.signals import *
11 |
12 | __all__ = ['CommentView']
13 |
14 |
15 | class CommentView(FlaskView):
16 | route_base = '/comment'
17 |
18 | @route(r'/add/', methods=['POST'])
19 | @login_required
20 | def add(self):
21 | form = CommentAddForm()
22 | if form.validate_on_submit():
23 | data = form.data
24 | print data
25 | target_type = str(data['target_type'])
26 | target_id = str(data['target_id'])
27 |
28 | comment = Comment()
29 | comment.user = current_user
30 | comment.content = data['content']
31 | comment.comment_target = target_type
32 | if target_type == 'answer':
33 | comment.answer = Answer.find_by_id(target_id, abort404=True)
34 |
35 | answer_comment_add.send(comment.answer)
36 |
37 | db.session.add(comment)
38 | db.session.commit()
39 | elif target_type == 'question':
40 | comment.question = Question.find_by_id(target_id, abort404=True)
41 |
42 | question_comment_add.send(comment.question)
43 |
44 | db.session.add(comment)
45 | db.session.commit()
46 | elif target_type == 'collection':
47 | comment.collection = Collection.find_by_id(target_id, abort404=True)
48 |
49 | #collection_comment_add.send(comment.collection)
50 |
51 | db.session.add(comment)
52 | db.session.commit()
53 | elif target_type == 'comment':
54 | comment.quote_comment = Comment.find_by_id(target_id, abort404=True)
55 | db.session.add(comment)
56 | db.session.commit()
57 | else:
58 | abort(500)
59 |
60 | if request.referrer:
61 | return redirect(request.referrer)
62 | else:
63 | return redirect('/question')
64 | else:
65 | print form.data
66 | abort(404)
67 |
68 | @route(r'//voteup/')
69 | @login_required
70 | def voteup(self, id):
71 | comment = Comment.find_by_id(id, abort404=True)
72 | current_user.voteup_comment(comment)
73 | db.session.commit()
74 |
75 | comment_voteup.send(comment)
76 |
77 | return redirect(request.referrer or '/')
78 |
79 |
80 | @route(r'//cancel_vote/')
81 | @login_required
82 | def cancel_vote(self, id):
83 | comment = Comment.find_by_id(id, abort404=True)
84 | current_user.voteup_comment(comment, undo=True)
85 | db.session.commit()
86 |
87 | comment_cancel_vote.send(comment)
88 |
89 | return redirect(request.referrer or '/')
90 |
--------------------------------------------------------------------------------
/tests/test_orm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask import Flask
5 |
6 | from FlaskZhihu.settings import TestSettings
7 | from FlaskZhihu.models import *
8 | from FlaskZhihu.extensions import db
9 |
10 | import unittest
11 |
12 | class OrmTestCase(unittest.TestCase):
13 | def setUp(self):
14 | app = Flask(__name__)
15 | app.config.from_object(TestSettings())
16 | db.init_app(app)
17 |
18 | ctx = app.app_context()
19 |
20 | ctx.push()
21 | db.create_all()
22 | session = db.session()
23 |
24 | self.app = app
25 | self.db = db
26 | self.ctx = ctx
27 | self.session = session
28 | print "setUp"
29 |
30 | def tearDown(self):
31 | self.db.drop_all()
32 | self.ctx.pop()
33 |
34 |
35 | def test_1_create_user(self):
36 | user1 = User()
37 | user1.id = 1
38 | user1.username = 'shn7798_1'
39 | user1.password = '123456'
40 | user1.name = 'shn7798_1'
41 |
42 | user2 = User()
43 | user2.id = 2
44 | user2.username = 'shn7798_2'
45 | user2.password = '123456'
46 | user2.name = 'shn7798_2'
47 |
48 | self.session.add_all([user1, user2])
49 | self.session.commit()
50 |
51 | u1 = User.query.filter(User.username=='shn7798_1').first_or_404()
52 | u2 = User.query.filter(User.id==2).first_or_404()
53 |
54 | self.assertEqual(user1, u1)
55 | self.assertEqual(user2, u2)
56 |
57 | self.user1 = user1
58 | self.user2 = user2
59 |
60 | def test_2_create_question(self):
61 | # 创建问题1
62 | question1 = Question()
63 | question1.id = 1
64 | question1.title = 'question 1'
65 | question1.content = 'question 1 content'
66 | question1.user = self.user1
67 | self.session.add(question1)
68 | self.session.commit()
69 |
70 | q1 = Question.query.filter(Question.id==1).first_or_404()
71 | self.assertEqual(q1.title, 'question 1')
72 | self.assertEqual(q1.content, 'question 1 content')
73 | self.assertEqual(q1.user_id, self.user1.id)
74 |
75 | self.question1 = question1
76 |
77 | def test_3_create_answer(self):
78 | # user2 创建答案
79 | answer1 = Answer()
80 | answer1.id = 11
81 | answer1.content = 'question 1 answer'
82 | answer1.user = self.user2
83 | answer1.question = self.question1
84 |
85 | self.session.add(answer1)
86 | self.session.commit()
87 |
88 | a1 = Answer.query.filter(Answer.id==11).first_or_404()
89 | self.assertEqual(a1.content, 'question 1 answer')
90 | self.assertEqual(a1.user_id, self.user2.id)
91 |
92 | def test_4_user_follow_question(self):
93 | self.user1.op_on_questions.append(self.question1)
94 | uoq = self.user1.op_on_question(self.question1)
95 | uoq.follow = True
96 |
97 | self.session.commit()
98 | assert self.user1 in self.question1.following_users()
99 |
100 |
101 |
102 | if __name__ == '__main__':
103 | unittest.main()
104 |
105 |
--------------------------------------------------------------------------------
/FlaskZhihu/views/answer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 | from flask import url_for, abort, redirect, render_template, request
4 | from flask.ext.classy import FlaskView, route
5 | from flask.ext.login import current_user, login_required
6 |
7 | from FlaskZhihu.extensions import db
8 | from FlaskZhihu.forms import AnswerAddForm, AnswerEditForm
9 | from FlaskZhihu.models import Answer, Question, User
10 | from FlaskZhihu.signals import *
11 | from FlaskZhihu.helpers import keep_next_url
12 |
13 | __all__ = ['AnswerView']
14 |
15 |
16 | class AnswerView(FlaskView):
17 | route_base = '/answer'
18 |
19 | @route(r'/add/', methods=['POST'])
20 | @login_required
21 | def add(self):
22 | form = AnswerAddForm()
23 | if form.validate_on_submit():
24 | question = Question.query.get_or_404(form.question_id.data)
25 | if Answer.query.filter(Answer.question_id == question.id).filter(Answer.user_id == current_user.id).count():
26 | # answer exists
27 | return redirect(url_for('QuestionView:show', id=str(form.question_id.data)))
28 | answer = Answer()
29 | answer.content = form.content.data
30 | answer.question = question
31 | answer.user = current_user
32 | db.session.add(answer)
33 | db.session.commit()
34 |
35 | question_answer_add.send(question)
36 |
37 | if form.question_id.data:
38 | return redirect(url_for('QuestionView:show', id=str(form.question_id.data)))
39 | else:
40 | abort(404)
41 |
42 | @route(r'//')
43 | def show(self, id):
44 | answer = Answer.find_by_id(id, abort404=True)
45 |
46 | return render_template('answer/show.html', answer=answer)
47 |
48 | @route(r'//voteup/')
49 | @login_required
50 | def voteup(self, id):
51 | answer = Answer.find_by_id(id, abort404=True)
52 | current_user.voteup_answer(answer)
53 | db.session.commit()
54 |
55 | answer_voteup.send(answer)
56 |
57 | return redirect(request.referrer or '/')
58 |
59 | @route(r'//votedown/')
60 | @login_required
61 | def votedown(self, id):
62 | answer = Answer.find_by_id(id, abort404=True)
63 | current_user.votedown_answer(answer)
64 | db.session.commit()
65 |
66 | answer_votedown.send(answer)
67 |
68 | return redirect(request.referrer or '/')
69 |
70 | @route(r'//cancel_vote/')
71 | @login_required
72 | def cancel_vote(self, id):
73 | answer = Answer.find_by_id(id, abort404=True)
74 | current_user.voteup_answer(answer, undo=True)
75 | current_user.votedown_answer(answer, undo=True)
76 | db.session.commit()
77 |
78 | answer_cancel_vote.send(answer)
79 |
80 | return redirect(request.referrer or '/')
81 |
82 |
83 | @route(r'/my/')
84 | @login_required
85 | def my(self):
86 | assert current_user.id
87 | answers = Answer.query.options(db.joinedload('question'))\
88 | .filter(Answer.user_id == current_user.id).all()
89 |
90 | return render_template('answer/my.html', answers=answers)
91 |
92 |
93 | @route(r'//edit/', methods=['GET', 'POST'])
94 | @login_required
95 | @keep_next_url
96 | def edit(self, id, next_url=None):
97 | answer = Answer.query.get_or_404(id)
98 | form = AnswerEditForm()
99 | if form.validate_on_submit():
100 | answer.content = form.content.data
101 | db.session.commit()
102 | return redirect(next_url or url_for('QuestionView:show', id=answer.question_id))
103 | else:
104 | form.content.data = answer.content
105 | return render_template('answer/edit.html', form=form)
--------------------------------------------------------------------------------
/tests/orm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask import Flask
5 |
6 | from FlaskZhihu.settings import IPythonSettings
7 | from FlaskZhihu.models import *
8 | from FlaskZhihu.extensions import db, cache
9 | from FlaskZhihu.application import create_app
10 | from flask.ext.sqlalchemy_cache import FromCache
11 | from flask.ext.sqlalchemy import get_debug_queries, get_state
12 |
13 |
14 | Base = db.Model
15 | Column = db.Column
16 | Integer = db.Integer
17 | ForeignKey = db.ForeignKey
18 | String = db.String
19 | relationship = db.relationship
20 | backref=db.backref
21 |
22 | #
23 | # class Node1(Base):
24 | # __tablename__ = 'node1'
25 | # id = Column(Integer, primary_key=True)
26 | # parent_id = Column(Integer, ForeignKey('node1.id'))
27 | # data = Column(String(50))
28 | # parent = relationship("Node1", remote_side=[id])
29 | #
30 | # class Node2(Base):
31 | # __tablename__ = 'node2'
32 | # id = Column(Integer, primary_key=True)
33 | # parent_id = Column(Integer, ForeignKey('node2.id'))
34 | # data = Column(String(50))
35 | # children = relationship("Node2",
36 | # backref=backref('parent', remote_side=[id])
37 | # )
38 |
39 |
40 |
41 | def init(db):
42 |
43 | db.create_all()
44 | session = db.session()
45 |
46 | user1 = User()
47 | user1.id = 1
48 | user1.username = 'shn7798_1'
49 | user1.password = '123456'
50 | user1.name = 'shn7798_1'
51 |
52 | user2 = User()
53 | user2.id = 2
54 | user2.username = 'shn7798_2'
55 | user2.password = '123456'
56 | user2.name = 'shn7798_2'
57 |
58 | user3 = User()
59 | user3.id = 3
60 | user3.username = 'shn7798_3'
61 | user3.password = '123456'
62 | user3.name = 'shn7798_3'
63 |
64 | session.add_all([user1, user2, user3])
65 | session.commit()
66 |
67 | u1 = User.query.filter(User.username == 'shn7798_1').first_or_404()
68 | u2 = User.query.filter(User.id == 2).first_or_404()
69 |
70 | assert (user1 == u1)
71 | assert (user2 == u2)
72 |
73 | # 创建问题1
74 | question1 = Question()
75 | question1.id = 1
76 | question1.title = 'question 1'
77 | question1.content = 'question 1 content'
78 | question1.user = user1
79 | session.add(question1)
80 | session.commit()
81 |
82 | q1 = Question.query.filter(Question.id == 1).first_or_404()
83 | assert (q1.title == 'question 1')
84 | assert (q1.content == 'question 1 content')
85 | assert (q1.user_id == user1.id)
86 |
87 | # user2 创建答案
88 | answer1 = Answer()
89 | answer1.id = 11
90 | answer1.content = 'question 1 answer'
91 | answer1.user = user2
92 | answer1.question = question1
93 |
94 | session.add(answer1)
95 | session.commit()
96 |
97 | a1 = Answer.query.filter(Answer.id == 11).first_or_404()
98 | assert (a1.content == 'question 1 answer')
99 | assert (a1.user_id == user2.id)
100 |
101 | user1.op_on_questions.append(question1)
102 | uoq = user1.op_on_question(question1)
103 | uoq.follow = True
104 |
105 | session.commit()
106 | assert user1 in question1.following_users()
107 |
108 | op = user1.op_on_user(user2, edit=True)
109 | op.follow = True
110 | session.commit()
111 |
112 | user1.voteup_answer(answer1)
113 | session.commit()
114 |
115 | user1.block_user(user3)
116 | session.commit()
117 |
118 |
119 | if __name__ == '__main__':
120 | app = create_app(IPythonSettings())
121 |
122 | ctx = app.app_context()
123 |
124 | ctx.push()
125 | session = db.session()
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/FlaskZhihu/views/question.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | import random
5 | import time
6 | import os
7 |
8 |
9 | from flask import abort, render_template, current_app, redirect, url_for
10 |
11 | from flask.ext.classy import FlaskView, route
12 | from flask.ext.login import login_required, current_user
13 |
14 | from flask.ext.sqlalchemy_cache import FromCache, RelationshipCache
15 | from flask.ext.sqlalchemy import get_debug_queries
16 |
17 | from FlaskZhihu.models import *
18 | from FlaskZhihu.helpers import cached
19 | from FlaskZhihu.extensions import cache
20 | from FlaskZhihu.forms import QuestionAddForm, QuestionEditForm
21 | from FlaskZhihu.helpers import keep_next_url
22 |
23 | from pprint import pprint
24 |
25 | __all__ = ['QuestionView']
26 |
27 |
28 |
29 | class QuestionView(FlaskView):
30 | route_base = '/question'
31 |
32 | def _random_questions(self, limit=20, max=None):
33 | if max is None:
34 | # max = Question.query.count() 会全表扫描
35 | max = db.session.query(db.func.count(Question.id)).first()[0]
36 | random.seed('%s%d' %(os.umask(24), time.time()))
37 | if max > limit:
38 | offset = random.randint(0, max - limit)
39 | else:
40 | offset = 0
41 | random_questions = Question.query.offset(offset).limit(limit)
42 | random_questions = sorted(random_questions, key=lambda x: x.answers_count, reverse=True)
43 | return random_questions
44 |
45 | @route(r'/')
46 | def index(self):
47 | random_questions = self._random_questions(20)
48 | return render_template('question/index.html',
49 | random_questions=random_questions)
50 |
51 |
52 | @route(r'//')
53 | def show(self, id):
54 | question = Question.query.filter(Question.id == int(id)).first_or_404()
55 | # .join(Answer.user_id == User.id)\
56 | answers = Answer.query\
57 | .options(db.joinedload('user'))\
58 | .filter(Answer.question_id == int(question.id)) \
59 | .all()
60 |
61 | random_questions = self._random_questions(20)
62 | response = render_template('question/show.html',
63 | question=question, answers=answers,
64 | random_questions=random_questions)
65 |
66 | #pprint(get_debug_queries())
67 | #print len(get_debug_queries())
68 |
69 | return response
70 |
71 | @route(r'/add/', methods=['GET', 'POST'])
72 | @login_required
73 | def add(self):
74 | form = QuestionAddForm()
75 | if form.validate_on_submit():
76 | question = Question()
77 | question.user = current_user
78 | question.title = form.title.data
79 | question.content = form.content.data
80 |
81 | db.session.add(question)
82 | db.session.commit()
83 |
84 | return redirect(url_for('QuestionView:show', id=question.id))
85 |
86 | return render_template('question/add.html', form=form)
87 |
88 | @route(r'/my/')
89 | @login_required
90 | def my(self):
91 | assert current_user.id
92 | questions = Question.query.filter(Question.user_id == current_user.id).all()
93 | return render_template('question/my.html', questions=questions)
94 |
95 | @route(r'//edit/', methods=['GET', 'POST'])
96 | @login_required
97 | @keep_next_url
98 | def edit(self, id, next_url=None):
99 | print id
100 | question = Question.query.get_or_404(id)
101 | form = QuestionEditForm()
102 | if form.validate_on_submit():
103 | question.title = form.title.data
104 | question.content = form.content.data
105 | db.session.commit()
106 | return redirect(next_url or url_for('QuestionView:show', id=question.id))
107 | else:
108 | form.title.data = question.title
109 | form.content.data = question.content
110 | return render_template('question/edit.html', form=form)
--------------------------------------------------------------------------------
/FlaskZhihu/views/collection.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask import url_for, abort, redirect, render_template, request
5 | from flask.ext.classy import FlaskView, route
6 | from flask.ext.login import current_user, login_required
7 |
8 | from FlaskZhihu.extensions import db
9 | from FlaskZhihu.forms import CollectionAddForm, CollectionEditForm
10 | from FlaskZhihu.models import Answer, Question, User, Collection
11 | from FlaskZhihu import signals
12 | from FlaskZhihu.helpers import keep_next_url
13 |
14 | __all__ = ['CollectionView']
15 |
16 |
17 | class CollectionView(FlaskView):
18 | route_base = '/collection'
19 |
20 | @route(r'//')
21 | def show(self, id):
22 | collection = Collection.query.options(db.joinedload(Collection.answers)).get_or_404(id)
23 | return render_template('collection/show.html', collection=collection)
24 |
25 | @route(r'/add/', methods=['GET', 'POST'])
26 | @login_required
27 | def add(self):
28 | form = CollectionAddForm()
29 | if form.validate_on_submit():
30 | collection = Collection()
31 | collection.user = current_user
32 | collection.title = form.title.data
33 | collection.description = form.description.data
34 |
35 | db.session.add(collection)
36 | db.session.commit()
37 |
38 | return redirect(url_for('CollectionView:my'))
39 |
40 | return render_template('collection/add.html', form=form)
41 |
42 | @route(r'//delete/', methods=['GET', 'POST'])
43 | @login_required
44 | @keep_next_url
45 | def delete(self, id, next_url=None):
46 | collection = Collection.query.filter(
47 | db.and_(
48 | Collection.id == id,
49 | Collection.user_id == current_user.id,
50 | )
51 | ).first_or_404()
52 |
53 | collection.answers = []
54 | db.session.delete(collection)
55 | db.session.commit()
56 | signals.collection_answer_delete.send(collection)
57 | return redirect(next_url or url_for('CollectionView:my'))
58 |
59 |
60 | @route(r'//edit/', methods=['GET', 'POST'])
61 | @login_required
62 | def edit(self, id):
63 | collection = Collection.query.filter(
64 | db.and_(
65 | Collection.id == id,
66 | Collection.user_id == current_user.id
67 | )
68 | ).first_or_404()
69 |
70 | form = CollectionEditForm()
71 |
72 | if form.validate_on_submit():
73 | if form.title.data:
74 | collection.title = form.title.data
75 | if form.description.data:
76 | collection.description = form.description.data
77 |
78 | db.session.commit()
79 | return redirect(url_for('CollectionView:show', id=collection.id))
80 | else:
81 | form.title.data = collection.title
82 | form.description.data = collection.description
83 | return render_template('collection/edit.html', form=form)
84 |
85 |
86 | @route(r'/my/')
87 | @login_required
88 | def my(self):
89 | collections = current_user.collections
90 | return render_template('collection/my.html', collections=collections)
91 |
92 |
93 | @route(r'//answer//add/', methods=['GET', 'POST'])
94 | @login_required
95 | @keep_next_url
96 | def add_answer(self, id, answer_id, next_url=None):
97 | collection = Collection.query.filter(
98 | db.and_(
99 | Collection.id == id,
100 | Collection.user_id == current_user.id
101 | )
102 | ).first_or_404()
103 | answer = Answer.query.get_or_404(answer_id)
104 | collection.answers.append(answer)
105 | db.session.commit()
106 |
107 | signals.collection_answer_add.send(collection)
108 | return redirect(next_url or url_for('CollectionView:show', id=collection.id))
109 |
110 |
111 | @route(r'//answer//delete/', methods=['GET', 'POST'])
112 | @login_required
113 | @keep_next_url
114 | def delete_answer(self, id, answer_id, next_url=None):
115 | collection = Collection.query.filter(
116 | db.and_(
117 | Collection.id == id,
118 | Collection.user_id == current_user.id
119 | )
120 | ).first_or_404()
121 | answer = Answer.query.get_or_404(answer_id)
122 | collection.answers.remove(answer)
123 | db.session.commit()
124 |
125 | signals.collection_answer_delete.send(collection)
126 | return redirect(next_url or url_for('CollectionView:show', id=collection.id))
127 |
128 |
129 | @route(r'/select/by/answer//')
130 | @login_required
131 | @keep_next_url
132 | def select_by_answer(self, answer_id, next_url=None):
133 | collections = current_user.collections
134 | return render_template('collection/select_by_answer.html', collections=collections, answer_id=answer_id)
--------------------------------------------------------------------------------
/FlaskZhihu/views/user.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from flask import abort, render_template, redirect, request, url_for
5 | from flask.ext.classy import FlaskView, route
6 | from flask.ext.login import login_user, login_required, logout_user, current_user
7 |
8 | from FlaskZhihu.models import User, Collection, UserOnCollection, Answer
9 | from FlaskZhihu.forms import UserLoginForm, UserAddForm, UserEditForm
10 | from FlaskZhihu.extensions import db, cache
11 | from FlaskZhihu.helpers import keep_next_url
12 | from FlaskZhihu.constants import FOLLOW_OFF, FOLLOW_ON
13 | from FlaskZhihu import signals
14 |
15 | __all__ = ['UserView']
16 |
17 | class UserView(FlaskView):
18 | route_base = '/user'
19 |
20 | @route('/login/', methods=['GET', 'POST'])
21 | @keep_next_url
22 | def login(self, next_url=None):
23 | form = UserLoginForm()
24 |
25 | if form.validate_on_submit():
26 | user = User.query.filter(User.username==form.username.data).first()
27 | if user and user.check_password(form.password.data):
28 | login_user(user)
29 |
30 | return redirect(next_url or '/')
31 |
32 | return render_template('user/login.html', form=form)
33 |
34 |
35 | @route('/logout/')
36 | @login_required
37 | def logout(self):
38 | logout_user()
39 | return redirect(request.referrer or '/')
40 |
41 |
42 | @route('/add/', methods=['GET', 'POST'])
43 | @keep_next_url
44 | def add(self, next_url=None):
45 | if current_user.is_anonymous:
46 | form = UserAddForm()
47 | if form.validate_on_submit():
48 | if User.query.filter(User.username==form.username.data).count() == 0:
49 | user = User()
50 | user.name = form.name.data
51 | user.username = form.username.data
52 | user.password = form.password.data
53 | db.session.add(user)
54 | db.session.commit()
55 | return redirect(url_for('UserView:login'))
56 | return render_template('user/add.html', form=form)
57 | else:
58 | return redirect(next_url or '/')
59 |
60 | @route('/edit/', methods=['GET', 'POST'])
61 | @login_required
62 | @keep_next_url
63 | def edit(self, next_url=None):
64 | form = UserEditForm()
65 | if form.validate_on_submit():
66 | user = current_user
67 | if form.name.data:
68 | user.name = form.name.data
69 | if form.password.data:
70 | user.password = form.password.data
71 | db.session.commit()
72 | # 不管有没有修改, 都返回上个页面
73 | return redirect(next_url or '/')
74 | else:
75 | if request.method.upper() == 'POST':
76 | return redirect(next_url or '/')
77 | else:
78 | return render_template('user/edit.html', form=form)
79 |
80 |
81 | @route('/collection//follow/')
82 | @login_required
83 | @keep_next_url
84 | def follow_collection(self, id, next_url=None):
85 | collection = Collection.query.get_or_404(id)
86 | current_user.follow_collection(collection)
87 | db.session.commit()
88 |
89 | signals.collection_follow.send(collection)
90 | return redirect(next_url or url_for('CollectionView:my'))
91 |
92 |
93 | @route('/collection//unfollow/')
94 | @login_required
95 | @keep_next_url
96 | def unfollow_collection(self, id, next_url=None):
97 | collection = Collection.query.get_or_404(id)
98 | current_user.unfollow_collection(collection)
99 | db.session.commit()
100 |
101 | signals.collection_unfollow.send(collection)
102 | return redirect(next_url or url_for('CollectionView:my'))
103 |
104 | @route('/my/')
105 | @login_required
106 | def my(self):
107 | return self.show(current_user.id)
108 |
109 | @route(r'//')
110 | def show(self, id):
111 | user = User.query.get_or_404(id)
112 | return render_template('user/show.html', user=user)
113 |
114 | @route(r'/answer//thank/', methods=['GET', 'POST'])
115 | @login_required
116 | @keep_next_url
117 | def thank_answer(self, answer_id, next_url=None):
118 | answer = Answer.query.get_or_404(answer_id)
119 | current_user.thank_answer(answer)
120 | db.session.commit()
121 |
122 | signals.answer_thank.send(answer)
123 | return redirect(next_url or url_for('AnswerView:show', id=answer_id))
124 |
125 | @route(r'/answer//unthank/', methods=['GET', 'POST'])
126 | @login_required
127 | @keep_next_url
128 | def unthank_answer(self, answer_id, next_url=None):
129 | answer = Answer.query.get_or_404(answer_id)
130 | current_user.unthank_answer(answer)
131 |
132 | db.session.commit()
133 |
134 | signals.answer_unthank.send(answer)
135 | return redirect(next_url or url_for('AnswerView:show', id=answer_id))
136 |
137 |
--------------------------------------------------------------------------------
/FlaskZhihu/models/signals.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.extensions import db
5 | from FlaskZhihu.models import *
6 | from FlaskZhihu.constants import *
7 | from FlaskZhihu.signals import *
8 | from FlaskZhihu.helpers import use_signal
9 |
10 | @use_signal(question_answer_add)
11 | def update_question_answers_count(sender):
12 | assert isinstance(sender, Question)
13 | sender.answers_count = \
14 | Answer.query.filter(Answer.question_id == sender.id).count()
15 |
16 | db.session.commit()
17 |
18 | @use_signal(question_follow)
19 | @use_signal(question_unfollow)
20 | def update_question_following_count(sender):
21 | assert isinstance(sender, Question)
22 | sender.following_count = \
23 | UserOnQuestion.query.filter(
24 | db.and_(UserOnQuestion.question_id == sender.id, UserOnQuestion.follow == True)
25 | ).count()
26 |
27 | db.session.commit()
28 |
29 |
30 | @use_signal(question_comment_add)
31 | @use_signal(question_comment_delete)
32 | def update_question_comments_count(sender):
33 | assert isinstance(sender, Question)
34 | sender.comments_count = \
35 | Comment.query.filter(
36 | Comment.question_id == sender.id
37 | ).count()
38 |
39 | db.session.commit()
40 |
41 | ############# answer ############
42 | @use_signal(answer_comment_add)
43 | @use_signal(answer_comment_delete)
44 | def update_answer_comments_count(sender):
45 | assert isinstance(sender, Answer)
46 | sender.comments_count = \
47 | Comment.query.filter(
48 | Comment.answer_id == sender.id
49 | ).count()
50 |
51 | db.session.commit()
52 |
53 |
54 | @use_signal(answer_voteup)
55 | @use_signal(answer_votedown)
56 | @use_signal(answer_cancel_vote)
57 | def update_answer_vote_count(sender):
58 | assert isinstance(sender, Answer)
59 | ops = UserOnAnswer.query.filter(
60 | db.and_(
61 | UserOnAnswer.answer_id == sender.id,
62 | UserOnAnswer.vote != VOTE_NONE,
63 | )
64 | ).all()
65 |
66 | sender.voteup_count = len(filter(lambda x: x.vote == VOTE_UP, ops))
67 | sender.votedown_count = len(filter(lambda x: x.vote == VOTE_DOWN, ops))
68 | db.session.commit()
69 |
70 |
71 | @use_signal(answer_thank)
72 | @use_signal(answer_unthank)
73 | def update_answer_thanks_count(sender):
74 | assert isinstance(sender, Answer)
75 | thanks_count = UserOnAnswer.query.filter(
76 | db.and_(
77 | UserOnAnswer.answer_id == sender.id,
78 | UserOnAnswer.thank == THANK_ON,
79 | )
80 | ).count()
81 |
82 | sender.thanks_count = thanks_count
83 | db.session.commit()
84 |
85 |
86 | ######### comment
87 | @use_signal(comment_voteup)
88 | @use_signal(comment_cancel_vote)
89 | def update_comment_vote_count(sender):
90 | assert isinstance(sender, Comment)
91 | voteup_count = UserOnComment.query.filter(
92 | db.and_(
93 | UserOnComment.comment_id == sender.id,
94 | UserOnComment.vote == VOTE_UP,
95 | )
96 | ).count()
97 |
98 | sender.voteup_count = voteup_count
99 | db.session.commit()
100 |
101 |
102 | ############# collection ############
103 |
104 | @use_signal(collection_answer_add)
105 | @use_signal(collection_answer_delete)
106 | def update_collection_answers_count(sender):
107 | assert isinstance(sender, Collection)
108 | answers_count = CollectionAndAnswer.query.filter(
109 | CollectionAndAnswer.collection_id == sender.id
110 | ).count()
111 |
112 | sender.answers_count = answers_count
113 | db.session.commit()
114 |
115 |
116 | @use_signal(collection_follow)
117 | @use_signal(collection_unfollow)
118 | def update_collection_following_count(sender):
119 | assert isinstance(sender, Collection)
120 | following_count = UserOnCollection.query.filter(
121 | db.and_(
122 | UserOnCollection.collection_id == sender.id,
123 | UserOnCollection.follow == FOLLOW_ON,
124 | )
125 | ).count()
126 |
127 | sender.following_count = following_count
128 | db.session.commit()
129 |
130 |
131 | @use_signal(answer_voteup)
132 | @use_signal(answer_votedown)
133 | @use_signal(answer_cancel_vote)
134 | def update_user_voteup_count(sender):
135 | assert isinstance(sender, Answer)
136 | user = sender.user
137 |
138 | if user:
139 | voteup_count, votedown_count = db.session.execute(
140 | 'select sum(voteup_count), sum(votedown_count) from answer where user_id = :user_id'
141 | , dict(user_id=user.id)
142 | ).first()
143 |
144 | user.voteup_count = voteup_count or 0
145 | user.votedown_count = votedown_count or 0
146 | db.session.commit()
147 |
148 |
149 | @use_signal(answer_thank)
150 | @use_signal(answer_unthank)
151 | def update_user_thanks_count(sender):
152 | assert isinstance(sender, Answer)
153 | user = sender.user
154 |
155 | if user:
156 | thanks_count = db.session.execute(
157 | 'select sum(thanks_count) from answer where user_id = :user_id'
158 | , dict(user_id=user.id)
159 | ).first()[0]
160 |
161 | user.thanks_count = thanks_count or 0
162 | db.session.commit()
163 |
164 |
--------------------------------------------------------------------------------
/FlaskZhihu/models/user/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __author__ = 'shn7798'
3 |
4 | from FlaskZhihu.extensions import db
5 | from FlaskZhihu.models.base import DateTimeMixin, FindByIdMixin
6 | from .operation import *
7 | from werkzeug.security import generate_password_hash, check_password_hash
8 | from flask.ext.sqlalchemy_cache import CachingQuery
9 |
10 |
11 | class UserOnUser(DateTimeMixin, FindByIdMixin, db.Model):
12 | __tablename__ = 'user_on_user'
13 |
14 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
15 | user_id = db.Column('user_id', db.ForeignKey(u'user.id'), nullable=False, index=True)
16 | dest_user_id = db.Column('dest_user_id', db.ForeignKey(u'user.id'), nullable=False, index=True)
17 | follow = db.Column('follow', db.Integer)
18 | block = db.Column('block', db.Integer)
19 |
20 |
21 | class UserLoginMixin():
22 | @property
23 | def is_authenticated(self):
24 | return True
25 |
26 | @property
27 | def is_active(self):
28 | return True
29 |
30 | @property
31 | def is_anonymous(self):
32 | return False
33 |
34 | def get_id(self):
35 | return self.id
36 |
37 |
38 | class User(
39 | UserOperationMixin, QuestionOperationMixin, AnswerOperationMixin, CollectionOperationMixin,
40 | CommentOperationMixin,
41 | DateTimeMixin, FindByIdMixin,
42 | UserLoginMixin,
43 | db.Model):
44 |
45 | __tablename__ = 'user'
46 | query_class = CachingQuery
47 |
48 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
49 | user_hashid = db.Column('user_hashid', db.String(32))
50 | username = db.Column('username', db.String(45), unique=True)
51 | name = db.Column('name', db.String(45))
52 | gender = db.Column('gender', db.Integer)
53 | description = db.Column('description', db.String(2048))
54 | headline = db.Column('headline', db.String(200))
55 | avatar_url = db.Column('avatar_url', db.String(200))
56 |
57 | # count cache
58 | voteup_count = db.Column('voteup_count', db.Integer, server_default='0')
59 | votedown_count = db.Column('votedown_count', db.Integer, server_default='0')
60 | thanks_count = db.Column('thanks_count', db.Integer, server_default='0')
61 |
62 | answers = db.relationship(u'Answer', backref='user')
63 | collections = db.relationship(u'Collection', backref='user')
64 | comments = db.relationship(u'Comment', backref='user')
65 | questions = db.relationship(u'Question', backref='user')
66 |
67 | _password = db.Column('password', db.String(100), nullable=False)
68 |
69 | def _set_password(self, password):
70 | self._password = generate_password_hash(password)
71 |
72 | def _get_password(self):
73 | return self._password
74 |
75 | password = db.synonym("_password",
76 | descriptor=property(_get_password,
77 | _set_password))
78 |
79 | op_on_answers = db.relationship(u'Answer', secondary='user_on_answer', backref='op_by_users')
80 | user_on_answer = db.relationship(u'UserOnAnswer', backref='user')
81 |
82 | op_on_collections = db.relationship(u'Collection', secondary='user_on_collection', backref='op_by_users')
83 | user_on_collection = db.relationship(u'UserOnCollection', backref='user')
84 |
85 | op_on_comments = db.relationship(u'Comment', secondary='user_on_comment', backref='op_by_users')
86 | user_on_comment = db.relationship(u'UserOnComment', backref='user')
87 |
88 | op_on_questions = db.relationship(u'Question', secondary='user_on_question', backref='op_by_users')
89 | user_on_question = db.relationship(u'UserOnQuestion', backref='user')
90 |
91 | op_on_topics = db.relationship(u'Topic', secondary='user_on_topic', backref='op_by_users')
92 | user_on_topic = db.relationship(u'UserOnTopic', backref='user')
93 |
94 | op_on_users = db.relationship(u'User', secondary='user_on_user',
95 | primaryjoin=id==UserOnUser.user_id,
96 | secondaryjoin=id==UserOnUser.dest_user_id,
97 | backref='op_by_users')
98 |
99 | user_on_dest_user = db.relationship(u'UserOnUser', foreign_keys=[UserOnUser.user_id], backref='user')
100 | dest_user_on_user = db.relationship(u'UserOnUser', foreign_keys=[UserOnUser.dest_user_id], backref='dest_user')
101 |
102 | @staticmethod
103 | def get_admin(cls):
104 | return User.query.filter(User.id==1).first_or_404()
105 |
106 | @staticmethod
107 | def get_user_by_hashid(hashid):
108 | return User.query.filter(User.user_hashid==hashid).first()
109 |
110 | def check_password(self, password):
111 | assert self._get_password()
112 | return check_password_hash(self._get_password(), password)
113 |
114 | def op_on_answer(self, answer, edit=False):
115 | return self._op_on_x(answer, UserOnAnswer, edit=edit)
116 |
117 | def op_on_collection(self, collection, edit=False):
118 | return self._op_on_x(collection, UserOnCollection, edit=edit)
119 |
120 | def op_on_comment(self, comment, edit=False):
121 | return self._op_on_x(comment, UserOnComment, edit=edit)
122 |
123 | def op_on_question(self, question, edit=False):
124 | return self._op_on_x(question, UserOnQuestion, edit=edit)
125 |
126 | def op_on_topic(self, topic, edit=False):
127 | return self._op_on_x(topic, UserOnTopic, edit=edit)
128 |
129 | def op_on_user(self, user, edit=False):
130 | return self._op_on_x(user, UserOnUser, op_fk=UserOnUser.dest_user_id, one_to_many=self.op_on_users, edit=edit)
131 |
132 | def _op_on_x(self, x_obj, op_table, op_fk=None, one_to_many=None, edit=False):
133 | x_name = x_obj.__class__.__name__
134 | if not op_fk:
135 | op_fk_name = '%s_id' % x_name.lower()
136 | op_fk = getattr(op_table, op_fk_name)
137 | if not one_to_many:
138 | one_to_many_name = 'op_on_%ss' % x_name.lower()
139 | one_to_many = getattr(self, one_to_many_name)
140 |
141 | op = op_table.query.filter(
142 | db.and_(op_table.user_id == self.id, op_fk == x_obj.id)
143 | ).first()
144 | if not op:
145 | if edit:
146 | one_to_many.append(x_obj)
147 | return self._op_on_x(x_obj, op_table, op_fk=op_fk, one_to_many=one_to_many)
148 | else:
149 | return None
150 | else:
151 | return op
152 |
153 | def __repr__(self):
154 | return '<%s %r>' % (self.__class__.__name__, self.username)
155 |
156 |
157 | class UserOnAnswer(DateTimeMixin, FindByIdMixin, db.Model):
158 | __tablename__ = 'user_on_answer'
159 |
160 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
161 | answer_id = db.Column('answer_id', db.ForeignKey(u'answer.id'), nullable=False, index=True)
162 | user_id = db.Column('user_id', db.ForeignKey(u'user.id'), nullable=False, index=True)
163 | follow = db.Column('follow', db.Integer)
164 | thank = db.Column('thank', db.Integer)
165 | vote = db.Column('vote', db.Integer)
166 |
167 |
168 | class UserOnCollection(DateTimeMixin, FindByIdMixin, db.Model):
169 | __tablename__ = 'user_on_collection'
170 |
171 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
172 | user_id = db.Column('user_id', db.ForeignKey(u'user.id'), nullable=False, index=True)
173 | collection_id = db.Column('collection_id', db.ForeignKey(u'collection.id'), nullable=False, index=True)
174 | public = db.Column('public', db.Integer, server_default=db.text("'0'"))
175 | follow = db.Column('follow', db.Integer, server_default=db.text("'0'"))
176 |
177 |
178 | class UserOnComment(DateTimeMixin, FindByIdMixin, db.Model):
179 | __tablename__ = 'user_on_comment'
180 |
181 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
182 | user_id = db.Column('user_id', db.ForeignKey(u'user.id'), nullable=False, index=True)
183 | comment_id = db.Column('comment_id', db.ForeignKey(u'comment.id'), nullable=False, index=True)
184 | vote = db.Column('vote', db.Integer)
185 |
186 |
187 | class UserOnQuestion(DateTimeMixin, FindByIdMixin, db.Model):
188 | __tablename__ = 'user_on_question'
189 |
190 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
191 | user_id = db.Column('user_id', db.ForeignKey(u'user.id'), nullable=False, index=True)
192 | question_id = db.Column('question_id', db.ForeignKey(u'question.id'), nullable=False, index=True)
193 | follow = db.Column('follow', db.Integer)
194 |
195 |
196 | class UserOnTopic(DateTimeMixin, FindByIdMixin, db.Model):
197 | __tablename__ = 'user_on_topic'
198 |
199 | id = db.Column('id', db.Integer, primary_key=True, autoincrement=True)
200 | user_id = db.Column('user_id', db.ForeignKey(u'user.id'), nullable=False, index=True)
201 | topic_id = db.Column('topic_id', db.ForeignKey(u'topic.id'), nullable=False, index=True)
202 | follow = db.Column('follow', db.Integer)
203 |
204 |
--------------------------------------------------------------------------------