├── .gitignore ├── LICENSE ├── README.rst ├── app ├── __init__.py ├── admin │ ├── __init__.py │ ├── forms.py │ └── views.py ├── api_1_0 │ ├── __init__.py │ ├── authentication.py │ ├── comments.py │ ├── decorators.py │ ├── errors.py │ ├── posts.py │ └── users.py ├── auth │ ├── __init__.py │ ├── forms.py │ └── views.py ├── decorators.py ├── delete.py ├── email.py ├── exceptions.py ├── main │ ├── __init__.py │ ├── errors.py │ ├── forms.py │ └── views.py ├── models.py ├── static │ ├── css │ │ ├── AdminLTE.min.css │ │ ├── bootstrap.min.css │ │ ├── ionicons.min.css │ │ ├── skin-blue.min.css │ │ └── styles.css │ ├── img │ │ ├── bridge.jpg │ │ ├── build.jpg │ │ ├── castle-1037355_1280.jpg │ │ ├── castle-1037355_1920.jpg │ │ ├── cat.jpg │ │ ├── dropbox-color@2x.png │ │ ├── favicon.ico │ │ ├── golden.jpg │ │ ├── gotop.png │ │ ├── me.jpg │ │ ├── other.jpg │ │ ├── stones-1149008_1280.jpg │ │ ├── stones-1149008_1920.jpg │ │ ├── top.jpg │ │ ├── top_sheimu.jpg │ │ ├── top_wind.jpg │ │ ├── top_钢丝.jpg │ │ ├── wind.jpg │ │ └── zhihu.jpg │ └── js │ │ ├── app.min.js │ │ ├── bootstrap.min.js │ │ ├── jquery-1.10.1.min.js │ │ ├── jquery-2.1.3.min.js │ │ ├── jquery-2.2.3.min.js │ │ ├── jquery-ui.min.js │ │ ├── jquery.Jcrop.min.js │ │ ├── jquery.cookie.js │ │ ├── jquery.form.js │ │ ├── jquery.from.js │ │ ├── jquery_from.min.js │ │ └── moment-with-langs.min.js ├── tasks │ ├── __init__.py │ └── celerymail.py └── templates │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── _comments.html │ ├── _comments_moderate.html │ ├── _index_posts.html │ ├── _macros.html │ ├── _message.html │ ├── _notice.html │ ├── _posts.html │ ├── _userbase.html │ ├── _webpush.html │ ├── aboutme.html │ ├── admin │ ├── addadmin.html │ ├── addcategory.html │ ├── adduser.html │ ├── edit.html │ ├── editcategory.html │ ├── editcomment.html │ ├── editpost.html │ └── edituser.html │ ├── auth │ ├── change_email.html │ ├── change_password.html │ ├── change_userset.html │ ├── email │ │ ├── change_email.html │ │ ├── change_email.txt │ │ ├── confirm.html │ │ ├── confirm.txt │ │ ├── reset_password.html │ │ └── reset_password.txt │ ├── login.html │ ├── register.html │ ├── reset_password.html │ └── unconfirmed.html │ ├── base.html │ ├── base2.html │ ├── base3.html │ ├── bootstrap_base.html │ ├── category.html │ ├── edit_post.html │ ├── edit_profile.html │ ├── error_page.html │ ├── followers.html │ ├── index.html │ ├── mail │ ├── new_user.html │ └── new_user.txt │ ├── moderate.html │ ├── post.html │ ├── search_results.html │ ├── sendmessage.html │ ├── showmessage.html │ ├── shownotice.html │ ├── unconfirmed.html │ ├── user.html │ ├── user_comments.html │ ├── user_showwebpush.html │ ├── user_starposts.html │ ├── video.html │ └── writepost.html ├── celery_worker.py ├── centos_config ├── centos_requirements.txt ├── nginx_default.conf ├── nolog_restartweb.sh ├── redis ├── restartweb.sh ├── stopweb.sh └── win_requirements.txt ├── config.ini ├── config.py ├── manage.py ├── requirements ├── common.txt ├── dev.txt ├── heroku.txt └── prod.txt ├── tests ├── __init__.py ├── test_api.py ├── test_basics.py ├── test_client.py ├── test_selenium.py └── test_user_model.py └── weibo.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.exe 3 | __pycache__/ 4 | migrations/ 5 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Miguel Grinberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Blog 2 | ---- 3 | A simple blog system based on Flask 4 | 5 | 6 | Development 7 | ----------- 8 | 9 | Prerequests: 10 | 11 | 1. python2.7/python3.4 12 | 2. mysql5.5+ 13 | 3. Reference: Flask Web开发-基于Python的Web应用开发实战 `http://www.ituring.com.cn/book/1449` 14 | 15 | Setup flask development: 16 | $ git clone `https://github.com/ifwenvlook/blog.git` 17 | 18 | $ cd /blog 19 | 20 | $ pip install -r requirements/dev.txt 21 | 22 | 23 | 24 | Quick Start Blog 25 | ----------- 26 | Run Mysql、redit and celery: 27 | $service redis start 28 | 29 | $service mysqld start 30 | 31 | $celery worker -A celery_worker.celery -l INFO & 32 | 33 | Create testdata and upgrade to mysql: 34 | $ python manage.py db init 35 | 36 | $ python manage.py db migrate 37 | 38 | $ python manage.py db upgrade 39 | 40 | $ python manage.py datainit 41 | 42 | $ python manage.py runserver 43 | 44 | 45 | 46 | Visit: `http://127.0.0.1:5000/` 47 | 48 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask.ext.bootstrap import Bootstrap 3 | from flask.ext.mail import Mail 4 | from flask.ext.moment import Moment 5 | from flask.ext.sqlalchemy import SQLAlchemy 6 | from flask.ext.login import LoginManager 7 | from flask.ext.pagedown import PageDown 8 | from config import config,Config 9 | from celery import Celery 10 | 11 | 12 | bootstrap = Bootstrap() 13 | mail = Mail() 14 | moment = Moment() 15 | db = SQLAlchemy() 16 | pagedown = PageDown() 17 | 18 | login_manager = LoginManager() 19 | login_manager.session_protection = 'strong' 20 | login_manager.login_view = 'auth.login' 21 | celery = Celery(__name__, broker=Config.CELERY_BROKER_URL) 22 | 23 | 24 | def create_app(config_name): 25 | app = Flask(__name__) 26 | app.config.from_object(config[config_name]) 27 | config[config_name].init_app(app) 28 | 29 | bootstrap.init_app(app) 30 | mail.init_app(app) 31 | moment.init_app(app) 32 | db.init_app(app) 33 | login_manager.init_app(app) 34 | pagedown.init_app(app) 35 | celery.conf.update(app.config) 36 | 37 | if not app.debug and not app.testing and not app.config['SSL_DISABLE']: 38 | from flask.ext.sslify import SSLify 39 | sslify = SSLify(app) 40 | 41 | from .main import main as main_blueprint 42 | app.register_blueprint(main_blueprint) 43 | 44 | from .auth import auth as auth_blueprint 45 | app.register_blueprint(auth_blueprint, url_prefix='/auth') 46 | 47 | from .api_1_0 import api as api_1_0_blueprint 48 | app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0') 49 | 50 | from .tasks import tasks as tasks_blueprint 51 | app.register_blueprint( tasks_blueprint , url_prefix='/tasks') 52 | 53 | from .admin import admin as admin_blueprint 54 | app.register_blueprint( admin_blueprint , url_prefix='/admin') 55 | 56 | return app 57 | -------------------------------------------------------------------------------- /app/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | admin = Blueprint('admin', __name__) 4 | 5 | from . import views 6 | from ..models import Permission 7 | 8 | 9 | @admin.app_context_processor 10 | def inject_permissions(): 11 | return dict(Permission=Permission) 12 | -------------------------------------------------------------------------------- /app/admin/forms.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | from flask.ext.wtf import Form 3 | from wtforms import StringField, PasswordField, BooleanField, SubmitField 4 | from wtforms.validators import Required, Length, Email, Regexp, EqualTo 5 | from wtforms import ValidationError 6 | from ..models import User, Category 7 | 8 | 9 | class AddadminForm(Form): 10 | username = StringField('用户名', validators=[ 11 | Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 12 | 'Usernames must have only letters, ' 13 | 'numbers, dots or underscores')]) 14 | password = PasswordField('密码', validators=[ 15 | Required(), EqualTo('password2', message='Passwords must match.')]) 16 | password2 = PasswordField('确认密码', validators=[Required()]) 17 | submit = SubmitField('添加') 18 | 19 | def validate_username(self, field): 20 | if User.query.filter_by(username=field.data).first(): 21 | raise ValidationError('用户名已存在') 22 | 23 | 24 | class AdduserForm(Form): 25 | email = StringField('邮箱', validators=[Required(), Length(1, 64), 26 | Email()]) 27 | username = StringField('用户名', validators=[ 28 | Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 29 | 'Usernames must have only letters, ' 30 | 'numbers, dots or underscores')]) 31 | password = PasswordField('密码', validators=[ 32 | Required(), EqualTo('password2', message='Passwords must match.')]) 33 | password2 = PasswordField('确认密码', validators=[Required()]) 34 | submit = SubmitField('添加') 35 | 36 | def validate_email(self, field): 37 | if User.query.filter_by(email=field.data).first(): 38 | raise ValidationError('Email已经被注册过.请更换') 39 | 40 | def validate_username(self, field): 41 | if User.query.filter_by(username=field.data).first(): 42 | raise ValidationError('用户名已存在') 43 | 44 | 45 | class AddcategoryForm(Form): 46 | name = StringField('新的分类', validators=[ 47 | Required(), Length(1, 64)]) 48 | submit = SubmitField('添加') 49 | 50 | def validate_username(self, field): 51 | if Category.query.filter_by(name=field.data).first(): 52 | raise ValidationError('分类已存在') -------------------------------------------------------------------------------- /app/admin/views.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | from flask import render_template, redirect, url_for, abort, flash, request,\ 3 | current_app, session, g 4 | from flask.ext.login import login_required, current_user 5 | from ..models import Permission, Role, User, Post, Comment, Message, Category, Star, Webpush 6 | from ..decorators import admin_required, permission_required 7 | from .. import db 8 | from . import admin 9 | from .forms import AddadminForm, AdduserForm, AddcategoryForm 10 | 11 | 12 | @admin.route('/', methods=['GET', 'POST']) 13 | @login_required 14 | def edit(): 15 | page = request.args.get('page', 1, type=int) 16 | pagination = User.query.filter_by(role_id=2).order_by(User.member_since.desc()).paginate( 17 | page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], 18 | error_out=False) 19 | admins = pagination.items 20 | return render_template('admin/edit.html',admins=admins,pagination=pagination, page=page) 21 | 22 | 23 | @admin.route('/admin2user/', methods=['GET', 'POST']) 24 | @login_required 25 | @admin_required 26 | def admin2user(id): 27 | user = User.query.get_or_404(id) 28 | user.role = Role.query.filter_by(name='User').first() 29 | db.session.add(user) 30 | flash ('已将" '+user.username+' "降为普通用户') 31 | return redirect(url_for('.edit')) 32 | 33 | @admin.route('/addadmin', methods=['GET', 'POST']) 34 | @login_required 35 | @admin_required 36 | def addadmin(): 37 | form = AddadminForm() 38 | if form.validate_on_submit(): 39 | user = User(email=form.username.data+'@vlblog.com', 40 | username=form.username.data, 41 | password=form.password.data, 42 | confirmed=True, 43 | role=Role.query.filter_by(permissions=0xff).first()) 44 | db.session.add(user) 45 | db.session.commit() 46 | flash('已添加" '+user.username+' "为管理员') 47 | return redirect(url_for('.edit')) 48 | return render_template('admin/addadmin.html',form=form) 49 | 50 | 51 | @admin.route('/edituser', methods=['GET', 'POST']) 52 | @login_required 53 | @admin_required 54 | def edituser(): 55 | page = request.args.get('page', 1, type=int) 56 | pagination = User.query.order_by(User.member_since.desc()).paginate( 57 | page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], 58 | error_out=False) 59 | users = pagination.items 60 | return render_template('admin/edituser.html',users=users,pagination=pagination, page=page) 61 | 62 | @admin.route('/deleteuser/', methods=['GET', 'POST']) 63 | @login_required 64 | @admin_required 65 | def deleteuser(id): 66 | user = User.query.get_or_404(id) 67 | 68 | posts = user.posts 69 | for post in posts: 70 | db.session.delete(post) 71 | comments = user.comments 72 | for comment in comments: 73 | db.session.delete(comment) 74 | messages = user.messages 75 | for message in messages: 76 | db.session.delete(message) 77 | webpushs = user.webpushs 78 | for webpush in webpushs: 79 | db.session.delete(webpush) 80 | 81 | db.session.delete(user) 82 | flash ('已将和" '+user.username+' "相关的内容删除') 83 | return redirect(url_for('.edituser')) 84 | 85 | @admin.route('/adduser', methods=['GET', 'POST']) 86 | @login_required 87 | @admin_required 88 | def adduser(): 89 | form = AdduserForm() 90 | if form.validate_on_submit(): 91 | user = User(email=form.email.data, 92 | username=form.username.data, 93 | password=form.password.data, 94 | confirmed=True) 95 | db.session.add(user) 96 | db.session.commit() 97 | flash('已添加" '+user.username+' "为普通用户') 98 | return redirect(url_for('.edituser')) 99 | return render_template('admin/adduser.html',form=form) 100 | 101 | @admin.route('/editpost', methods=['GET', 'POST']) 102 | @login_required 103 | @admin_required 104 | def editpost(): 105 | page = request.args.get('page', 1, type=int) 106 | pagination = Post.query.order_by(Post.timestamp.desc()).paginate( 107 | page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], 108 | error_out=False) 109 | posts = pagination.items 110 | return render_template('admin/editpost.html',posts=posts,pagination=pagination, page=page) 111 | 112 | @admin.route('/post/delete/') 113 | @login_required 114 | @admin_required 115 | def deletepost(id): 116 | post=Post.query.get_or_404(id) 117 | db.session.delete(post) 118 | for comment in post.comments: 119 | db.session.delete(comment) 120 | for webpush in post.webpushs: 121 | db.session.delete(webpush) 122 | flash('博客以及相关的评论、推送已删除') 123 | return redirect(url_for('.editpost')) 124 | 125 | @admin.route('/editcategory', methods=['GET', 'POST']) 126 | @login_required 127 | @admin_required 128 | def editcategory(): 129 | categorys = Category.query.order_by(Category.id).all() 130 | return render_template('admin/editcategory.html',categorys=categorys) 131 | 132 | @admin.route('/addcategory', methods=['GET', 'POST']) 133 | @login_required 134 | @admin_required 135 | def addcategory(): 136 | form = AddcategoryForm() 137 | if form.validate_on_submit(): 138 | category = Category(name=form.name.data) 139 | db.session.add(category) 140 | db.session.commit() 141 | flash('已添加" '+category.name+' "为新的分类') 142 | return redirect(url_for('.editcategory')) 143 | return render_template('admin/addcategory.html',form=form) 144 | 145 | 146 | 147 | @admin.route('/editcomment') 148 | @login_required 149 | @admin_required 150 | def editcomment(): 151 | page = request.args.get('page', 1, type=int) 152 | pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate( 153 | page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], 154 | error_out=False) 155 | comments = pagination.items 156 | return render_template('admin/editcomment.html', comments=comments, 157 | pagination=pagination, page=page, ) 158 | 159 | 160 | @admin.route('/deletecomment/') 161 | @login_required 162 | @admin_required 163 | def deletecomment_enable(id): 164 | comment = Comment.query.get_or_404(id) 165 | db.session.delete(comment) 166 | return redirect(url_for('.editcomment', 167 | page=request.args.get('page', 1, type=int)), ) 168 | 169 | @admin.route('/editcomment/enable/') 170 | @login_required 171 | @admin_required 172 | def editcomment_enable(id): 173 | comment = Comment.query.get_or_404(id) 174 | comment.disabled = False 175 | db.session.add(comment) 176 | return redirect(url_for('.editcomment', 177 | page=request.args.get('page', 1, type=int)), ) 178 | 179 | 180 | @admin.route('/editcomment/disable/') 181 | @login_required 182 | @admin_required 183 | def editcomment_disable(id): 184 | comment = Comment.query.get_or_404(id) 185 | comment.disabled = True 186 | db.session.add(comment) 187 | return redirect(url_for('.editcomment', 188 | page=request.args.get('page', 1, type=int)), ) 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /app/api_1_0/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | api = Blueprint('api', __name__) 4 | 5 | from . import authentication, posts, users, comments, errors 6 | -------------------------------------------------------------------------------- /app/api_1_0/authentication.py: -------------------------------------------------------------------------------- 1 | from flask import g, jsonify 2 | from flask.ext.httpauth import HTTPBasicAuth 3 | from ..models import User, AnonymousUser 4 | from . import api 5 | from .errors import unauthorized, forbidden 6 | 7 | auth = HTTPBasicAuth() 8 | 9 | 10 | @auth.verify_password 11 | def verify_password(email_or_token, password): 12 | if email_or_token == '': 13 | g.current_user = AnonymousUser() 14 | return True 15 | if password == '': 16 | g.current_user = User.verify_auth_token(email_or_token) 17 | g.token_used = True 18 | return g.current_user is not None 19 | user = User.query.filter_by(email=email_or_token).first() 20 | if not user: 21 | return False 22 | g.current_user = user 23 | g.token_used = False 24 | return user.verify_password(password) 25 | 26 | 27 | @auth.error_handler 28 | def auth_error(): 29 | return unauthorized('Invalid credentials') 30 | 31 | 32 | @api.before_request 33 | @auth.login_required 34 | def before_request(): 35 | if not g.current_user.is_anonymous and \ 36 | not g.current_user.confirmed: 37 | return forbidden('Unconfirmed account') 38 | 39 | 40 | @api.route('/token') 41 | def get_token(): 42 | if g.current_user.is_anonymous or g.token_used: 43 | return unauthorized('Invalid credentials') 44 | return jsonify({'token': g.current_user.generate_auth_token( 45 | expiration=3600), 'expiration': 3600}) 46 | -------------------------------------------------------------------------------- /app/api_1_0/comments.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, request, g, url_for, current_app 2 | from .. import db 3 | from ..models import Post, Permission, Comment 4 | from . import api 5 | from .decorators import permission_required 6 | 7 | 8 | @api.route('/comments/') 9 | def get_comments(): 10 | page = request.args.get('page', 1, type=int) 11 | pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate( 12 | page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], 13 | error_out=False) 14 | comments = pagination.items 15 | prev = None 16 | if pagination.has_prev: 17 | prev = url_for('api.get_comments', page=page-1, _external=True) 18 | next = None 19 | if pagination.has_next: 20 | next = url_for('api.get_comments', page=page+1, _external=True) 21 | return jsonify({ 22 | 'posts': [comment.to_json() for comment in comments], 23 | 'prev': prev, 24 | 'next': next, 25 | 'count': pagination.total 26 | }) 27 | 28 | 29 | @api.route('/comments/') 30 | def get_comment(id): 31 | comment = Comment.query.get_or_404(id) 32 | return jsonify(comment.to_json()) 33 | 34 | 35 | @api.route('/posts//comments/') 36 | def get_post_comments(id): 37 | post = Post.query.get_or_404(id) 38 | page = request.args.get('page', 1, type=int) 39 | pagination = post.comments.order_by(Comment.timestamp.asc()).paginate( 40 | page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], 41 | error_out=False) 42 | comments = pagination.items 43 | prev = None 44 | if pagination.has_prev: 45 | prev = url_for('api.get_comments', page=page-1, _external=True) 46 | next = None 47 | if pagination.has_next: 48 | next = url_for('api.get_comments', page=page+1, _external=True) 49 | return jsonify({ 50 | 'posts': [comment.to_json() for comment in comments], 51 | 'prev': prev, 52 | 'next': next, 53 | 'count': pagination.total 54 | }) 55 | 56 | 57 | @api.route('/posts//comments/', methods=['POST']) 58 | @permission_required(Permission.COMMENT) 59 | def new_post_comment(id): 60 | post = Post.query.get_or_404(id) 61 | comment = Comment.from_json(request.json) 62 | comment.author = g.current_user 63 | comment.post = post 64 | db.session.add(comment) 65 | db.session.commit() 66 | return jsonify(comment.to_json()), 201, \ 67 | {'Location': url_for('api.get_comment', id=comment.id, 68 | _external=True)} 69 | -------------------------------------------------------------------------------- /app/api_1_0/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from flask import g 3 | from .errors import forbidden 4 | 5 | 6 | def permission_required(permission): 7 | def decorator(f): 8 | @wraps(f) 9 | def decorated_function(*args, **kwargs): 10 | if not g.current_user.can(permission): 11 | return forbidden('Insufficient permissions') 12 | return f(*args, **kwargs) 13 | return decorated_function 14 | return decorator 15 | -------------------------------------------------------------------------------- /app/api_1_0/errors.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from app.exceptions import ValidationError 3 | from . import api 4 | 5 | 6 | def bad_request(message): 7 | response = jsonify({'error': 'bad request', 'message': message}) 8 | response.status_code = 400 9 | return response 10 | 11 | 12 | def unauthorized(message): 13 | response = jsonify({'error': 'unauthorized', 'message': message}) 14 | response.status_code = 401 15 | return response 16 | 17 | 18 | def forbidden(message): 19 | response = jsonify({'error': 'forbidden', 'message': message}) 20 | response.status_code = 403 21 | return response 22 | 23 | 24 | @api.errorhandler(ValidationError) 25 | def validation_error(e): 26 | return bad_request(e.args[0]) 27 | -------------------------------------------------------------------------------- /app/api_1_0/posts.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, request, g, abort, url_for, current_app 2 | from .. import db 3 | from ..models import Post, Permission 4 | from . import api 5 | from .decorators import permission_required 6 | from .errors import forbidden 7 | 8 | 9 | @api.route('/posts/') 10 | def get_posts(): 11 | page = request.args.get('page', 1, type=int) 12 | pagination = Post.query.paginate( 13 | page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], 14 | error_out=False) 15 | posts = pagination.items 16 | prev = None 17 | if pagination.has_prev: 18 | prev = url_for('api.get_posts', page=page-1, _external=True) 19 | next = None 20 | if pagination.has_next: 21 | next = url_for('api.get_posts', page=page+1, _external=True) 22 | return jsonify({ 23 | 'posts': [post.to_json() for post in posts], 24 | 'prev': prev, 25 | 'next': next, 26 | 'count': pagination.total 27 | }) 28 | 29 | 30 | @api.route('/posts/') 31 | def get_post(id): 32 | post = Post.query.get_or_404(id) 33 | return jsonify(post.to_json()) 34 | 35 | 36 | @api.route('/posts/', methods=['POST']) 37 | @permission_required(Permission.WRITE_ARTICLES) 38 | def new_post(): 39 | post = Post.from_json(request.json) 40 | post.author = g.current_user 41 | db.session.add(post) 42 | db.session.commit() 43 | return jsonify(post.to_json()), 201, \ 44 | {'Location': url_for('api.get_post', id=post.id, _external=True)} 45 | 46 | 47 | @api.route('/posts/', methods=['PUT']) 48 | @permission_required(Permission.WRITE_ARTICLES) 49 | def edit_post(id): 50 | post = Post.query.get_or_404(id) 51 | if g.current_user != post.author and \ 52 | not g.current_user.can(Permission.ADMINISTER): 53 | return forbidden('Insufficient permissions') 54 | post.body = request.json.get('body', post.body) 55 | db.session.add(post) 56 | return jsonify(post.to_json()) 57 | -------------------------------------------------------------------------------- /app/api_1_0/users.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, request, current_app, url_for 2 | from . import api 3 | from ..models import User, Post 4 | 5 | 6 | @api.route('/users/') 7 | def get_user(id): 8 | user = User.query.get_or_404(id) 9 | return jsonify(user.to_json()) 10 | 11 | 12 | @api.route('/users//posts/') 13 | def get_user_posts(id): 14 | user = User.query.get_or_404(id) 15 | page = request.args.get('page', 1, type=int) 16 | pagination = user.posts.order_by(Post.timestamp.desc()).paginate( 17 | page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], 18 | error_out=False) 19 | posts = pagination.items 20 | prev = None 21 | if pagination.has_prev: 22 | prev = url_for('api.get_posts', page=page-1, _external=True) 23 | next = None 24 | if pagination.has_next: 25 | next = url_for('api.get_posts', page=page+1, _external=True) 26 | return jsonify({ 27 | 'posts': [post.to_json() for post in posts], 28 | 'prev': prev, 29 | 'next': next, 30 | 'count': pagination.total 31 | }) 32 | 33 | 34 | @api.route('/users//timeline/') 35 | def get_user_followed_posts(id): 36 | user = User.query.get_or_404(id) 37 | page = request.args.get('page', 1, type=int) 38 | pagination = user.followed_posts.order_by(Post.timestamp.desc()).paginate( 39 | page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], 40 | error_out=False) 41 | posts = pagination.items 42 | prev = None 43 | if pagination.has_prev: 44 | prev = url_for('api.get_posts', page=page-1, _external=True) 45 | next = None 46 | if pagination.has_next: 47 | next = url_for('api.get_posts', page=page+1, _external=True) 48 | return jsonify({ 49 | 'posts': [post.to_json() for post in posts], 50 | 'prev': prev, 51 | 'next': next, 52 | 'count': pagination.total 53 | }) 54 | -------------------------------------------------------------------------------- /app/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | auth = Blueprint('auth', __name__) 4 | 5 | from . import views 6 | -------------------------------------------------------------------------------- /app/auth/forms.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | from flask.ext.wtf import Form 3 | from wtforms import StringField, PasswordField, BooleanField, SubmitField 4 | from wtforms.validators import Required, Length, Email, Regexp, EqualTo 5 | from wtforms import ValidationError 6 | from ..models import User 7 | 8 | 9 | class LoginForm(Form): 10 | email = StringField('邮箱', validators=[Required(), Length(1, 64), 11 | Email()]) 12 | password = PasswordField('密码', validators=[Required()]) 13 | remember_me = BooleanField('记住我') 14 | submit = SubmitField('登录') 15 | 16 | 17 | class RegistrationForm(Form): 18 | email = StringField('邮箱', validators=[Required(), Length(1, 64), 19 | Email()]) 20 | username = StringField('用户名', validators=[ 21 | Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 22 | 'Usernames must have only letters, ' 23 | 'numbers, dots or underscores')]) 24 | password = PasswordField('密码', validators=[ 25 | Required(), EqualTo('password2', message='Passwords must match.')]) 26 | password2 = PasswordField('确认密码', validators=[Required()]) 27 | submit = SubmitField('注册') 28 | 29 | def validate_email(self, field): 30 | if User.query.filter_by(email=field.data).first(): 31 | raise ValidationError('Email已经被注册过.请更换') 32 | 33 | def validate_username(self, field): 34 | if User.query.filter_by(username=field.data).first(): 35 | raise ValidationError('用户名已存在') 36 | 37 | 38 | class ChangePasswordForm(Form): 39 | old_password = PasswordField('旧密码', validators=[Required()]) 40 | password = PasswordField('新密码', validators=[ 41 | Required(), EqualTo('password2', message='Passwords must match')]) 42 | password2 = PasswordField('确认新的密码', validators=[Required()]) 43 | submit = SubmitField('更新密码') 44 | 45 | 46 | class PasswordResetRequestForm(Form): 47 | email = StringField('邮箱', validators=[Required(), Length(1, 64), 48 | Email()]) 49 | submit = SubmitField('重置密码') 50 | 51 | 52 | class PasswordResetForm(Form): 53 | email = StringField('邮箱', validators=[Required(), Length(1, 64), 54 | Email()]) 55 | password = PasswordField('新密码', validators=[ 56 | Required(), EqualTo('password2', message='Passwords must match')]) 57 | password2 = PasswordField('确认新的密码', validators=[Required()]) 58 | submit = SubmitField('重置密码') 59 | 60 | def validate_email(self, field): 61 | if User.query.filter_by(email=field.data).first() is None: 62 | raise ValidationError('不合法的Email地址') 63 | 64 | 65 | class ChangeEmailForm(Form): 66 | email = StringField('新邮箱', validators=[Required(), Length(1, 64), 67 | Email()]) 68 | password = PasswordField('密码', validators=[Required()]) 69 | submit = SubmitField('更新邮箱') 70 | 71 | def validate_email(self, field): 72 | if User.query.filter_by(email=field.data).first(): 73 | raise ValidationError('邮箱已被使用') 74 | -------------------------------------------------------------------------------- /app/auth/views.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, redirect, request, url_for, flash, g 2 | from flask.ext.login import login_user, logout_user, login_required, \ 3 | current_user 4 | import os 5 | 6 | from . import auth 7 | from .. import db 8 | from ..models import User,Post 9 | from ..email import send_email 10 | from .forms import LoginForm, RegistrationForm, ChangePasswordForm,\ 11 | PasswordResetRequestForm, PasswordResetForm, ChangeEmailForm 12 | from datetime import datetime 13 | 14 | 15 | 16 | 17 | 18 | @auth.before_app_request 19 | def before_request(): #定义全局变量 20 | # g.hot_post=Post().hotpost() 21 | # g.current_time=datetime.utcnow() 22 | if current_user.is_authenticated: 23 | current_user.ping() 24 | if not current_user.confirmed \ 25 | and request.endpoint[:5] != 'auth.' \ 26 | and request.endpoint != 'static': 27 | return render_template('auth/unconfirmed.html') 28 | 29 | 30 | @auth.route('/unconfirmed') 31 | def unconfirmed(): 32 | if current_user.is_anonymous or current_user.confirmed: 33 | return redirect(url_for('main.index')) 34 | return render_template('auth/unconfirmed.html') 35 | 36 | 37 | @auth.route('/login', methods=['GET', 'POST']) 38 | def login(): 39 | form = LoginForm() 40 | if form.validate_on_submit(): 41 | user = User.query.filter_by(email=form.email.data).first() 42 | if user is not None and user.verify_password(form.password.data): 43 | login_user(user, form.remember_me.data) 44 | return redirect(request.args.get('next') or url_for('main.index')) 45 | flash('Invalid username or password.') 46 | return render_template('auth/login.html', form=form) 47 | 48 | 49 | @auth.route('/logout') 50 | @login_required 51 | def logout(): 52 | logout_user() 53 | flash('You have been logged out.') 54 | return redirect(url_for('main.index')) 55 | 56 | 57 | @auth.route('/register', methods=['GET', 'POST']) 58 | def register(): 59 | form = RegistrationForm() 60 | if form.validate_on_submit(): 61 | user = User(email=form.email.data, 62 | username=form.username.data, 63 | password=form.password.data) 64 | db.session.add(user) 65 | db.session.commit() 66 | token = user.generate_confirmation_token() 67 | send_email(user.email, 'Confirm Your Account', 68 | 'auth/email/confirm', user=user, token=token) 69 | flash('A confirmation email has been sent to you by email.') 70 | return redirect(url_for('auth.login')) 71 | return render_template('auth/register.html', form=form) 72 | 73 | 74 | @auth.route('/confirm/') 75 | @login_required 76 | def confirm(token): 77 | if current_user.confirmed: 78 | return redirect(url_for('main.index')) 79 | if current_user.confirm(token): 80 | flash('You have confirmed your account. Thanks!') 81 | else: 82 | flash('The confirmation link is invalid or has expired.') 83 | return redirect(url_for('main.index')) 84 | 85 | 86 | @auth.route('/confirm') 87 | @login_required 88 | def resend_confirmation(): 89 | token = current_user.generate_confirmation_token() 90 | send_email(current_user.email, 'Confirm Your Account', 91 | 'auth/email/confirm', user=current_user, token=token) 92 | flash('A new confirmation email has been sent to you by email.') 93 | return redirect(url_for('main.index')) 94 | 95 | 96 | @auth.route('/change_userset', methods=['GET', 'POST']) 97 | @login_required 98 | def change_userset(): 99 | form = ChangePasswordForm() 100 | if form.validate_on_submit(): 101 | if current_user.verify_password(form.old_password.data): 102 | current_user.password = form.password.data 103 | db.session.add(current_user) 104 | flash('Your password has been updated.') 105 | return redirect(url_for('main.index')) 106 | else: 107 | flash('Invalid password.') 108 | return render_template("auth/change_userset.html", form=form) 109 | 110 | 111 | @auth.route('/change_password', methods=['GET', 'POST']) 112 | @login_required 113 | def change_password(): 114 | form = ChangePasswordForm() 115 | if form.validate_on_submit(): 116 | if current_user.verify_password(form.old_password.data): 117 | current_user.password = form.password.data 118 | db.session.add(current_user) 119 | flash('Your password has been updated.') 120 | return redirect(url_for('main.index')) 121 | else: 122 | flash('Invalid password.') 123 | return render_template("auth/change_password.html", form=form) 124 | 125 | 126 | @auth.route('/reset', methods=['GET', 'POST']) 127 | def password_reset_request(): 128 | if not current_user.is_anonymous: 129 | return redirect(url_for('main.index')) 130 | form = PasswordResetRequestForm() 131 | if form.validate_on_submit(): 132 | user = User.query.filter_by(email=form.email.data).first() 133 | if user: 134 | token = user.generate_reset_token() 135 | send_email(user.email, 'Reset Your Password', 136 | 'auth/email/reset_password', 137 | user=user, token=token, 138 | next=request.args.get('next')) 139 | flash('An email with instructions to reset your password has been ' 140 | 'sent to you.') 141 | return redirect(url_for('auth.login')) 142 | return render_template('auth/reset_password.html', form=form) 143 | 144 | 145 | @auth.route('/reset/', methods=['GET', 'POST']) 146 | def password_reset(token): 147 | if not current_user.is_anonymous: 148 | return redirect(url_for('main.index')) 149 | form = PasswordResetForm() 150 | if form.validate_on_submit(): 151 | user = User.query.filter_by(email=form.email.data).first() 152 | if user is None: 153 | return redirect(url_for('main.index')) 154 | if user.reset_password(token, form.password.data): 155 | flash('Your password has been updated.') 156 | return redirect(url_for('auth.login')) 157 | else: 158 | return redirect(url_for('main.index')) 159 | return render_template('auth/reset_password.html', form=form) 160 | 161 | 162 | @auth.route('/change-email', methods=['GET', 'POST']) 163 | @login_required 164 | def change_email_request(): 165 | form = ChangeEmailForm() 166 | if form.validate_on_submit(): 167 | if current_user.verify_password(form.password.data): 168 | new_email = form.email.data 169 | token = current_user.generate_email_change_token(new_email) 170 | send_email(new_email, 'Confirm your email address', 171 | 'auth/email/change_email', 172 | user=current_user, token=token) 173 | flash('An email with instructions to confirm your new email ' 174 | 'address has been sent to you.') 175 | return redirect(url_for('main.index')) 176 | else: 177 | flash('Invalid email or password.') 178 | return render_template("auth/change_email.html", form=form) 179 | 180 | 181 | @auth.route('/change-email/') 182 | @login_required 183 | def change_email(token): 184 | if current_user.change_email(token): 185 | flash('Your email address has been updated.') 186 | else: 187 | flash('Invalid request.') 188 | return redirect(url_for('main.index')) 189 | -------------------------------------------------------------------------------- /app/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from flask import abort 3 | from flask.ext.login import current_user 4 | from .models import Permission 5 | 6 | 7 | def permission_required(permission): 8 | def decorator(f): 9 | @wraps(f) 10 | def decorated_function(*args, **kwargs): 11 | if not current_user.can(permission): 12 | abort(403) 13 | return f(*args, **kwargs) 14 | return decorated_function 15 | return decorator 16 | 17 | 18 | def admin_required(f): 19 | return permission_required(Permission.ADMINISTER)(f) 20 | -------------------------------------------------------------------------------- /app/delete.py: -------------------------------------------------------------------------------- 1 | from .models import User 2 | from . import db 3 | 4 | def deletenone(): 5 | noneuser=User.query.filter_by(username=None).all() 6 | for user in noneuser: 7 | db.session.delete(user) 8 | db.session.commit() 9 | -------------------------------------------------------------------------------- /app/email.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from flask import current_app, render_template 3 | from flask.ext.mail import Message 4 | from . import mail 5 | 6 | 7 | def send_async_email(app, msg): 8 | with app.app_context(): 9 | mail.send(msg) 10 | 11 | 12 | def send_email(to, subject, template, **kwargs): 13 | app = current_app._get_current_object() 14 | msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject, 15 | sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to]) 16 | msg.body = render_template(template + '.txt', **kwargs) 17 | msg.html = render_template(template + '.html', **kwargs) 18 | thr = Thread(target=send_async_email, args=[app, msg]) 19 | thr.start() 20 | return thr 21 | -------------------------------------------------------------------------------- /app/exceptions.py: -------------------------------------------------------------------------------- 1 | class ValidationError(ValueError): 2 | pass 3 | -------------------------------------------------------------------------------- /app/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | main = Blueprint('main', __name__) 4 | 5 | from . import views, errors 6 | from ..models import Permission 7 | 8 | 9 | @main.app_context_processor 10 | def inject_permissions(): 11 | return dict(Permission=Permission) 12 | -------------------------------------------------------------------------------- /app/main/errors.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request, jsonify 2 | from . import main 3 | 4 | 5 | @main.app_errorhandler(403) 6 | def forbidden(e): 7 | if request.accept_mimetypes.accept_json and \ 8 | not request.accept_mimetypes.accept_html: 9 | response = jsonify({'error': 'forbidden'}) 10 | response.status_code = 403 11 | return response 12 | return render_template('403.html'), 403 13 | 14 | 15 | @main.app_errorhandler(404) 16 | def page_not_found(e): 17 | if request.accept_mimetypes.accept_json and \ 18 | not request.accept_mimetypes.accept_html: 19 | response = jsonify({'error': 'not found'}) 20 | response.status_code = 404 21 | return response 22 | return render_template('404.html'), 404 23 | 24 | 25 | @main.app_errorhandler(500) 26 | def internal_server_error(e): 27 | if request.accept_mimetypes.accept_json and \ 28 | not request.accept_mimetypes.accept_html: 29 | response = jsonify({'error': 'internal server error'}) 30 | response.status_code = 500 31 | return response 32 | return render_template('500.html'), 500 33 | -------------------------------------------------------------------------------- /app/main/forms.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | from flask.ext.wtf import Form 3 | from wtforms import StringField, TextAreaField, BooleanField, SelectField,\ 4 | SubmitField,FieldList 5 | from wtforms.validators import Required, Length, Email, Regexp,AnyOf 6 | from wtforms import ValidationError 7 | from flask.ext.pagedown.fields import PageDownField 8 | from ..models import Role, User, Message, Category 9 | 10 | 11 | 12 | class SendmessageForm(Form): 13 | body = StringField('私信内容', validators=[Length(0, 256)]) 14 | submit = SubmitField('发送') 15 | 16 | 17 | class NameForm(Form): 18 | name = StringField('你的名字', validators=[Required()]) 19 | submit = SubmitField('提交') 20 | 21 | 22 | class EditProfileForm(Form): 23 | name = StringField('姓名', validators=[Length(0, 64)]) 24 | location = StringField('地址', validators=[Length(0, 64)]) 25 | about_me = TextAreaField('关于我') 26 | submit = SubmitField('提交') 27 | 28 | class PostForm(Form): 29 | category = SelectField('文章类别', coerce=int) 30 | head = StringField('标题', validators=[Required(), Length(1, 25)]) 31 | body = PageDownField("正文", validators=[Required()]) 32 | submit = SubmitField('发布') 33 | 34 | 35 | def __init__(self, *args, **kwargs): #定义下拉选择表 36 | super(PostForm,self).__init__(*args, **kwargs) 37 | self.category.choices = [(category.id, category.name) 38 | for category in Category.query.order_by(Category.name).all()] 39 | 40 | 41 | class EditProfileAdminForm(Form): 42 | email = StringField('电子邮箱(Email)', validators=[Required(), Length(1, 64), 43 | Email()]) 44 | username = StringField('用户名', validators=[ 45 | Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 46 | 'Usernames must have only letters, ' 47 | 'numbers, dots or underscores')]) 48 | confirmed = BooleanField('Confirmed') 49 | role = SelectField('Role', coerce=int) 50 | name = StringField('姓名', validators=[Length(0, 64)]) 51 | location = StringField('地址', validators=[Length(0, 64)]) 52 | about_me = TextAreaField('关于我') 53 | submit = SubmitField('提交') 54 | 55 | def __init__(self, user, *args, **kwargs): 56 | super(EditProfileAdminForm, self).__init__(*args, **kwargs) 57 | self.role.choices = [(role.id, role.name) 58 | for role in Role.query.order_by(Role.name).all()] 59 | self.user = user 60 | 61 | def validate_email(self, field): 62 | if field.data != self.user.email and \ 63 | User.query.filter_by(email=field.data).first(): 64 | raise ValidationError('Email已经被使用.') 65 | 66 | def validate_username(self, field): 67 | if field.data != self.user.username and \ 68 | User.query.filter_by(username=field.data).first(): 69 | raise ValidationError('用户名已存在') 70 | 71 | class CommentForm(Form): 72 | body = StringField('输入你的评论', validators=[Required()]) 73 | submit = SubmitField('提交') 74 | 75 | 76 | class SearchForm(Form): 77 | search = StringField('search', validators = [Required()]) 78 | -------------------------------------------------------------------------------- /app/static/css/skin-blue.min.css: -------------------------------------------------------------------------------- 1 | .skin-blue .main-header .navbar{background-color:#3c8dbc}.skin-blue .main-header .navbar .nav>li>a{color:#fff}.skin-blue .main-header .navbar .nav>li>a:hover,.skin-blue .main-header .navbar .nav>li>a:active,.skin-blue .main-header .navbar .nav>li>a:focus,.skin-blue .main-header .navbar .nav .open>a,.skin-blue .main-header .navbar .nav .open>a:hover,.skin-blue .main-header .navbar .nav .open>a:focus,.skin-blue .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{background-color:#367fa9}@media (max-width:767px){.skin-blue .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue .main-header .navbar .dropdown-menu li a:hover{background:#367fa9}}.skin-blue .main-header .logo{background-color:#367fa9;color:#fff;border-bottom:0 solid transparent}.skin-blue .main-header .logo:hover{background-color:#357ca5}.skin-blue .main-header li.user-header{background-color:#3c8dbc}.skin-blue .content-header{background:transparent}.skin-blue .wrapper,.skin-blue .main-sidebar,.skin-blue .left-side{background-color:#222d32}.skin-blue .user-panel>.info,.skin-blue .user-panel>.info>a{color:#fff}.skin-blue .sidebar-menu>li.header{color:#4b646f;background:#1a2226}.skin-blue .sidebar-menu>li>a{border-left:3px solid transparent}.skin-blue .sidebar-menu>li:hover>a,.skin-blue .sidebar-menu>li.active>a{color:#fff;background:#1e282c;border-left-color:#3c8dbc}.skin-blue .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#2c3b41}.skin-blue .sidebar a{color:#b8c7ce}.skin-blue .sidebar a:hover{text-decoration:none}.skin-blue .treeview-menu>li>a{color:#8aa4af}.skin-blue .treeview-menu>li.active>a,.skin-blue .treeview-menu>li>a:hover{color:#fff}.skin-blue .sidebar-form{border-radius:3px;border:1px solid #374850;margin:10px 10px}.skin-blue .sidebar-form input[type="text"],.skin-blue .sidebar-form .btn{box-shadow:none;background-color:#374850;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-blue .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue .sidebar-form input[type="text"]:focus,.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-blue.layout-top-nav .main-header>.logo{background-color:#3c8dbc;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#3b8ab8} -------------------------------------------------------------------------------- /app/static/css/styles.css: -------------------------------------------------------------------------------- 1 | .profile-thumbnail { 2 | position: absolute; 3 | } 4 | .profile-header { 5 | min-height: 260px; 6 | margin-left: 280px; 7 | } 8 | div.post-tabs { 9 | margin-top: 16px; 10 | } 11 | ul.posts { 12 | list-style-type: none; 13 | padding: 0px; 14 | margin: 16px 10px 10px 10px; 15 | border-top: 1px solid #e0e0e0; 16 | } 17 | div.post-tabs ul.posts { 18 | margin: 0px; 19 | border-top: none; 20 | } 21 | ul.posts li.post { 22 | padding: 8px; 23 | border-bottom: 1px solid #e0e0e0; 24 | } 25 | ul.posts li.post:hover { 26 | background-color: #f0f0f0; 27 | } 28 | div.post-date { 29 | float: right; 30 | } 31 | div.post-author { 32 | font-weight: bold; 33 | } 34 | div.post-thumbnail { 35 | position: absolute; 36 | } 37 | div.post-content { 38 | margin-left: 48px; 39 | min-height: 48px; 40 | } 41 | div.post-footer { 42 | text-align: right; 43 | } 44 | ul.comments { 45 | list-style-type: none; 46 | padding: 0px; 47 | margin: 16px 0px 0px 0px; 48 | } 49 | ul.comments li.comment { 50 | margin-left: 32px; 51 | padding: 8px; 52 | border-bottom: 1px solid #e0e0e0; 53 | } 54 | ul.comments li.comment:nth-child(1) { 55 | border-top: 1px solid #e0e0e0; 56 | } 57 | ul.comments li.comment:hover { 58 | background-color: #f0f0f0; 59 | } 60 | div.comment-date { 61 | float: right; 62 | } 63 | div.comment-author { 64 | font-weight: bold; 65 | } 66 | div.comment-thumbnail { 67 | position: absolute; 68 | } 69 | div.comment-content { 70 | margin-left: 48px; 71 | min-height: 48px; 72 | } 73 | div.comment-form { 74 | margin: 16px 0px 16px 32px; 75 | } 76 | div.pagination { 77 | width: 100%; 78 | text-align: right; 79 | padding: 0px; 80 | margin: 0px; 81 | } 82 | div.flask-pagedown-preview { 83 | margin: 10px 0px 10px 0px; 84 | border: 1px solid #e0e0e0; 85 | padding: 4px; 86 | } 87 | div.flask-pagedown-preview h1 { 88 | font-size: 140%; 89 | } 90 | div.flask-pagedown-preview h2 { 91 | font-size: 130%; 92 | } 93 | div.flask-pagedown-preview h3 { 94 | font-size: 120%; 95 | } 96 | .post-body h1 { 97 | font-size: 140%; 98 | } 99 | .post-body h2 { 100 | font-size: 130%; 101 | } 102 | .post-body h3 { 103 | font-size: 120%; 104 | } 105 | .table.followers tr { 106 | border-bottom: 1px solid #e0e0e0; 107 | } 108 | -------------------------------------------------------------------------------- /app/static/img/bridge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/bridge.jpg -------------------------------------------------------------------------------- /app/static/img/build.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/build.jpg -------------------------------------------------------------------------------- /app/static/img/castle-1037355_1280.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/castle-1037355_1280.jpg -------------------------------------------------------------------------------- /app/static/img/castle-1037355_1920.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/castle-1037355_1920.jpg -------------------------------------------------------------------------------- /app/static/img/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/cat.jpg -------------------------------------------------------------------------------- /app/static/img/dropbox-color@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/dropbox-color@2x.png -------------------------------------------------------------------------------- /app/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/favicon.ico -------------------------------------------------------------------------------- /app/static/img/golden.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/golden.jpg -------------------------------------------------------------------------------- /app/static/img/gotop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/gotop.png -------------------------------------------------------------------------------- /app/static/img/me.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/me.jpg -------------------------------------------------------------------------------- /app/static/img/other.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/other.jpg -------------------------------------------------------------------------------- /app/static/img/stones-1149008_1280.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/stones-1149008_1280.jpg -------------------------------------------------------------------------------- /app/static/img/stones-1149008_1920.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/stones-1149008_1920.jpg -------------------------------------------------------------------------------- /app/static/img/top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/top.jpg -------------------------------------------------------------------------------- /app/static/img/top_sheimu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/top_sheimu.jpg -------------------------------------------------------------------------------- /app/static/img/top_wind.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/top_wind.jpg -------------------------------------------------------------------------------- /app/static/img/top_钢丝.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/top_钢丝.jpg -------------------------------------------------------------------------------- /app/static/img/wind.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/wind.jpg -------------------------------------------------------------------------------- /app/static/img/zhihu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/app/static/img/zhihu.jpg -------------------------------------------------------------------------------- /app/static/js/app.min.js: -------------------------------------------------------------------------------- 1 | /*! AdminLTE app.js 2 | * ================ 3 | * Main JS application file for AdminLTE v2. This file 4 | * should be included in all pages. It controls some layout 5 | * options and implements exclusive AdminLTE plugins. 6 | * 7 | * @Author Almsaeed Studio 8 | * @Support 9 | * @Email 10 | * @version 2.3.0 11 | * @license MIT 12 | */ 13 | function _init(){"use strict";$.AdminLTE.layout={activate:function(){var a=this;a.fix(),a.fixSidebar(),$(window,".wrapper").resize(function(){a.fix(),a.fixSidebar()})},fix:function(){var a=$(".main-header").outerHeight()+$(".main-footer").outerHeight(),b=$(window).height(),c=$(".sidebar").height();if($("body").hasClass("fixed"))$(".content-wrapper, .right-side").css("min-height",b-$(".main-footer").outerHeight());else{var d;b>=c?($(".content-wrapper, .right-side").css("min-height",b-a),d=b-a):($(".content-wrapper, .right-side").css("min-height",c),d=c);var e=$($.AdminLTE.options.controlSidebarOptions.selector);"undefined"!=typeof e&&e.height()>d&&$(".content-wrapper, .right-side").css("min-height",e.height())}},fixSidebar:function(){return $("body").hasClass("fixed")?("undefined"==typeof $.fn.slimScroll&&window.console&&window.console.error("Error: the fixed layout requires the slimscroll plugin!"),void($.AdminLTE.options.sidebarSlimScroll&&"undefined"!=typeof $.fn.slimScroll&&($(".sidebar").slimScroll({destroy:!0}).height("auto"),$(".sidebar").slimscroll({height:$(window).height()-$(".main-header").height()+"px",color:"rgba(0,0,0,0.2)",size:"3px"})))):void("undefined"!=typeof $.fn.slimScroll&&$(".sidebar").slimScroll({destroy:!0}).height("auto"))}},$.AdminLTE.pushMenu={activate:function(a){var b=$.AdminLTE.options.screenSizes;$(a).on("click",function(a){a.preventDefault(),$(window).width()>b.sm-1?$("body").hasClass("sidebar-collapse")?$("body").removeClass("sidebar-collapse").trigger("expanded.pushMenu"):$("body").addClass("sidebar-collapse").trigger("collapsed.pushMenu"):$("body").hasClass("sidebar-open")?$("body").removeClass("sidebar-open").removeClass("sidebar-collapse").trigger("collapsed.pushMenu"):$("body").addClass("sidebar-open").trigger("expanded.pushMenu")}),$(".content-wrapper").click(function(){$(window).width()<=b.sm-1&&$("body").hasClass("sidebar-open")&&$("body").removeClass("sidebar-open")}),($.AdminLTE.options.sidebarExpandOnHover||$("body").hasClass("fixed")&&$("body").hasClass("sidebar-mini"))&&this.expandOnHover()},expandOnHover:function(){var a=this,b=$.AdminLTE.options.screenSizes.sm-1;$(".main-sidebar").hover(function(){$("body").hasClass("sidebar-mini")&&$("body").hasClass("sidebar-collapse")&&$(window).width()>b&&a.expand()},function(){$("body").hasClass("sidebar-mini")&&$("body").hasClass("sidebar-expanded-on-hover")&&$(window).width()>b&&a.collapse()})},expand:function(){$("body").removeClass("sidebar-collapse").addClass("sidebar-expanded-on-hover")},collapse:function(){$("body").hasClass("sidebar-expanded-on-hover")&&$("body").removeClass("sidebar-expanded-on-hover").addClass("sidebar-collapse")}},$.AdminLTE.tree=function(a){var b=this,c=$.AdminLTE.options.animationSpeed;$(document).on("click",a+" li a",function(a){var d=$(this),e=d.next();if(e.is(".treeview-menu")&&e.is(":visible"))e.slideUp(c,function(){e.removeClass("menu-open")}),e.parent("li").removeClass("active");else if(e.is(".treeview-menu")&&!e.is(":visible")){var f=d.parents("ul").first(),g=f.find("ul:visible").slideUp(c);g.removeClass("menu-open");var h=d.parent("li");e.slideDown(c,function(){e.addClass("menu-open"),f.find("li.active").removeClass("active"),h.addClass("active"),b.layout.fix()})}e.is(".treeview-menu")&&a.preventDefault()})},$.AdminLTE.controlSidebar={activate:function(){var a=this,b=$.AdminLTE.options.controlSidebarOptions,c=$(b.selector),d=$(b.toggleBtnSelector);d.on("click",function(d){d.preventDefault(),c.hasClass("control-sidebar-open")||$("body").hasClass("control-sidebar-open")?a.close(c,b.slide):a.open(c,b.slide)});var e=$(".control-sidebar-bg");a._fix(e),$("body").hasClass("fixed")?a._fixForFixed(c):$(".content-wrapper, .right-side").height() .box-body, > .box-footer, > form >.box-body, > form > .box-footer");c.hasClass("collapsed-box")?(a.children(":first").removeClass(b.icons.open).addClass(b.icons.collapse),d.slideDown(b.animationSpeed,function(){c.removeClass("collapsed-box")})):(a.children(":first").removeClass(b.icons.collapse).addClass(b.icons.open),d.slideUp(b.animationSpeed,function(){c.addClass("collapsed-box")}))},remove:function(a){var b=a.parents(".box").first();b.slideUp(this.animationSpeed)}}}if("undefined"==typeof jQuery)throw new Error("AdminLTE requires jQuery");$.AdminLTE={},$.AdminLTE.options={navbarMenuSlimscroll:!0,navbarMenuSlimscrollWidth:"3px",navbarMenuHeight:"200px",animationSpeed:500,sidebarToggleSelector:"[data-toggle='offcanvas']",sidebarPushMenu:!0,sidebarSlimScroll:!0,sidebarExpandOnHover:!1,enableBoxRefresh:!0,enableBSToppltip:!0,BSTooltipSelector:"[data-toggle='tooltip']",enableFastclick:!0,enableControlSidebar:!0,controlSidebarOptions:{toggleBtnSelector:"[data-toggle='control-sidebar']",selector:".control-sidebar",slide:!0},enableBoxWidget:!0,boxWidgetOptions:{boxWidgetIcons:{collapse:"fa-minus",open:"fa-plus",remove:"fa-times"},boxWidgetSelectors:{remove:'[data-widget="remove"]',collapse:'[data-widget="collapse"]'}},directChat:{enable:!0,contactToggleSelector:'[data-widget="chat-pane-toggle"]'},colors:{lightBlue:"#3c8dbc",red:"#f56954",green:"#00a65a",aqua:"#00c0ef",yellow:"#f39c12",blue:"#0073b7",navy:"#001F3F",teal:"#39CCCC",olive:"#3D9970",lime:"#01FF70",orange:"#FF851B",fuchsia:"#F012BE",purple:"#8E24AA",maroon:"#D81B60",black:"#222222",gray:"#d2d6de"},screenSizes:{xs:480,sm:768,md:992,lg:1200}},$(function(){"use strict";$("body").removeClass("hold-transition"),"undefined"!=typeof AdminLTEOptions&&$.extend(!0,$.AdminLTE.options,AdminLTEOptions);var a=$.AdminLTE.options;_init(),$.AdminLTE.layout.activate(),$.AdminLTE.tree(".sidebar"),a.enableControlSidebar&&$.AdminLTE.controlSidebar.activate(),a.navbarMenuSlimscroll&&"undefined"!=typeof $.fn.slimscroll&&$(".navbar .menu").slimscroll({height:a.navbarMenuHeight,alwaysVisible:!1,size:a.navbarMenuSlimscrollWidth}).css("width","100%"),a.sidebarPushMenu&&$.AdminLTE.pushMenu.activate(a.sidebarToggleSelector),a.enableBSToppltip&&$("body").tooltip({selector:a.BSTooltipSelector}),a.enableBoxWidget&&$.AdminLTE.boxWidget.activate(),a.enableFastclick&&"undefined"!=typeof FastClick&&FastClick.attach(document.body),a.directChat.enable&&$(document).on("click",a.directChat.contactToggleSelector,function(){var a=$(this).parents(".direct-chat").first();a.toggleClass("direct-chat-contacts-open")}),$('.btn-group[data-toggle="btn-toggle"]').each(function(){var a=$(this);$(this).find(".btn").on("click",function(b){a.find(".btn.active").removeClass("active"),$(this).addClass("active"),b.preventDefault()})})}),function(a){"use strict";a.fn.boxRefresh=function(b){function c(a){a.append(f),e.onLoadStart.call(a)}function d(a){a.find(f).remove(),e.onLoadDone.call(a)}var e=a.extend({trigger:".refresh-btn",source:"",onLoadStart:function(a){return a},onLoadDone:function(a){return a}},b),f=a('
');return this.each(function(){if(""===e.source)return void(window.console&&window.console.log("Please specify a source first - boxRefresh()"));var b=a(this),f=b.find(e.trigger).first();f.on("click",function(a){a.preventDefault(),c(b),b.find(".box-body").load(e.source,function(){d(b)})})})}}(jQuery),function(a){"use strict";a.fn.activateBox=function(){a.AdminLTE.boxWidget.activate(this)}}(jQuery),function(a){"use strict";a.fn.todolist=function(b){var c=a.extend({onCheck:function(a){return a},onUncheck:function(a){return a}},b);return this.each(function(){"undefined"!=typeof a.fn.iCheck?(a("input",this).on("ifChecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onCheck.call(b)}),a("input",this).on("ifUnchecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onUncheck.call(b)})):a("input",this).on("change",function(){var b=a(this).parents("li").first();b.toggleClass("done"),a("input",b).is(":checked")?c.onCheck.call(b):c.onUncheck.call(b)})})}}(jQuery); -------------------------------------------------------------------------------- /app/static/js/jquery.Jcrop.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jquery.Jcrop.min.js v0.9.12 (build:20130202) 3 | * jQuery Image Cropping Plugin - released under MIT License 4 | * Copyright (c) 2008-2013 Tapmodo Interactive LLC 5 | * https://github.com/tapmodo/Jcrop 6 | */ 7 | (function(a){a.Jcrop=function(b,c){function i(a){return Math.round(a)+"px"}function j(a){return d.baseClass+"-"+a}function k(){return a.fx.step.hasOwnProperty("backgroundColor")}function l(b){var c=a(b).offset();return[c.left,c.top]}function m(a){return[a.pageX-e[0],a.pageY-e[1]]}function n(b){typeof b!="object"&&(b={}),d=a.extend(d,b),a.each(["onChange","onSelect","onRelease","onDblClick"],function(a,b){typeof d[b]!="function"&&(d[b]=function(){})})}function o(a,b,c){e=l(D),bc.setCursor(a==="move"?a:a+"-resize");if(a==="move")return bc.activateHandlers(q(b),v,c);var d=_.getFixed(),f=r(a),g=_.getCorner(r(f));_.setPressed(_.getCorner(f)),_.setCurrent(g),bc.activateHandlers(p(a,d),v,c)}function p(a,b){return function(c){if(!d.aspectRatio)switch(a){case"e":c[1]=b.y2;break;case"w":c[1]=b.y2;break;case"n":c[0]=b.x2;break;case"s":c[0]=b.x2}else switch(a){case"e":c[1]=b.y+1;break;case"w":c[1]=b.y+1;break;case"n":c[0]=b.x+1;break;case"s":c[0]=b.x+1}_.setCurrent(c),bb.update()}}function q(a){var b=a;return bd.watchKeys 8 | (),function(a){_.moveOffset([a[0]-b[0],a[1]-b[1]]),b=a,bb.update()}}function r(a){switch(a){case"n":return"sw";case"s":return"nw";case"e":return"nw";case"w":return"ne";case"ne":return"sw";case"nw":return"se";case"se":return"nw";case"sw":return"ne"}}function s(a){return function(b){return d.disabled?!1:a==="move"&&!d.allowMove?!1:(e=l(D),W=!0,o(a,m(b)),b.stopPropagation(),b.preventDefault(),!1)}}function t(a,b,c){var d=a.width(),e=a.height();d>b&&b>0&&(d=b,e=b/a.width()*a.height()),e>c&&c>0&&(e=c,d=c/a.height()*a.width()),T=a.width()/d,U=a.height()/e,a.width(d).height(e)}function u(a){return{x:a.x*T,y:a.y*U,x2:a.x2*T,y2:a.y2*U,w:a.w*T,h:a.h*U}}function v(a){var b=_.getFixed();b.w>d.minSelect[0]&&b.h>d.minSelect[1]?(bb.enableHandles(),bb.done()):bb.release(),bc.setCursor(d.allowSelect?"crosshair":"default")}function w(a){if(d.disabled)return!1;if(!d.allowSelect)return!1;W=!0,e=l(D),bb.disableHandles(),bc.setCursor("crosshair");var b=m(a);return _.setPressed(b),bb.update(),bc.activateHandlers(x,v,a.type.substring 9 | (0,5)==="touch"),bd.watchKeys(),a.stopPropagation(),a.preventDefault(),!1}function x(a){_.setCurrent(a),bb.update()}function y(){var b=a("
").addClass(j("tracker"));return g&&b.css({opacity:0,backgroundColor:"white"}),b}function be(a){G.removeClass().addClass(j("holder")).addClass(a)}function bf(a,b){function t(){window.setTimeout(u,l)}var c=a[0]/T,e=a[1]/U,f=a[2]/T,g=a[3]/U;if(X)return;var h=_.flipCoords(c,e,f,g),i=_.getFixed(),j=[i.x,i.y,i.x2,i.y2],k=j,l=d.animationDelay,m=h[0]-j[0],n=h[1]-j[1],o=h[2]-j[2],p=h[3]-j[3],q=0,r=d.swingSpeed;c=k[0],e=k[1],f=k[2],g=k[3],bb.animMode(!0);var s,u=function(){return function(){q+=(100-q)/r,k[0]=Math.round(c+q/100*m),k[1]=Math.round(e+q/100*n),k[2]=Math.round(f+q/100*o),k[3]=Math.round(g+q/100*p),q>=99.8&&(q=100),q<100?(bh(k),t()):(bb.done(),bb.animMode(!1),typeof b=="function"&&b.call(bs))}}();t()}function bg(a){bh([a[0]/T,a[1]/U,a[2]/T,a[3]/U]),d.onSelect.call(bs,u(_.getFixed())),bb.enableHandles()}function bh(a){_.setPressed([a[0],a[1]]),_.setCurrent([a[2], 10 | a[3]]),bb.update()}function bi(){return u(_.getFixed())}function bj(){return _.getFixed()}function bk(a){n(a),br()}function bl(){d.disabled=!0,bb.disableHandles(),bb.setCursor("default"),bc.setCursor("default")}function bm(){d.disabled=!1,br()}function bn(){bb.done(),bc.activateHandlers(null,null)}function bo(){G.remove(),A.show(),A.css("visibility","visible"),a(b).removeData("Jcrop")}function bp(a,b){bb.release(),bl();var c=new Image;c.onload=function(){var e=c.width,f=c.height,g=d.boxWidth,h=d.boxHeight;D.width(e).height(f),D.attr("src",a),H.attr("src",a),t(D,g,h),E=D.width(),F=D.height(),H.width(E).height(F),M.width(E+L*2).height(F+L*2),G.width(E).height(F),ba.resize(E,F),bm(),typeof b=="function"&&b.call(bs)},c.src=a}function bq(a,b,c){var e=b||d.bgColor;d.bgFade&&k()&&d.fadeTime&&!c?a.animate({backgroundColor:e},{queue:!1,duration:d.fadeTime}):a.css("backgroundColor",e)}function br(a){d.allowResize?a?bb.enableOnly():bb.enableHandles():bb.disableHandles(),bc.setCursor(d.allowSelect?"crosshair":"default"),bb 11 | .setCursor(d.allowMove?"move":"default"),d.hasOwnProperty("trueSize")&&(T=d.trueSize[0]/E,U=d.trueSize[1]/F),d.hasOwnProperty("setSelect")&&(bg(d.setSelect),bb.done(),delete d.setSelect),ba.refresh(),d.bgColor!=N&&(bq(d.shade?ba.getShades():G,d.shade?d.shadeColor||d.bgColor:d.bgColor),N=d.bgColor),O!=d.bgOpacity&&(O=d.bgOpacity,d.shade?ba.refresh():bb.setBgOpacity(O)),P=d.maxSize[0]||0,Q=d.maxSize[1]||0,R=d.minSize[0]||0,S=d.minSize[1]||0,d.hasOwnProperty("outerImage")&&(D.attr("src",d.outerImage),delete d.outerImage),bb.refresh()}var d=a.extend({},a.Jcrop.defaults),e,f=navigator.userAgent.toLowerCase(),g=/msie/.test(f),h=/msie [1-6]\./.test(f);typeof b!="object"&&(b=a(b)[0]),typeof c!="object"&&(c={}),n(c);var z={border:"none",visibility:"visible",margin:0,padding:0,position:"absolute",top:0,left:0},A=a(b),B=!0;if(b.tagName=="IMG"){if(A[0].width!=0&&A[0].height!=0)A.width(A[0].width),A.height(A[0].height);else{var C=new Image;C.src=A[0].src,A.width(C.width),A.height(C.height)}var D=A.clone().removeAttr("id"). 12 | css(z).show();D.width(A.width()),D.height(A.height()),A.after(D).hide()}else D=A.css(z).show(),B=!1,d.shade===null&&(d.shade=!0);t(D,d.boxWidth,d.boxHeight);var E=D.width(),F=D.height(),G=a("
").width(E).height(F).addClass(j("holder")).css({position:"relative",backgroundColor:d.bgColor}).insertAfter(A).append(D);d.addClass&&G.addClass(d.addClass);var H=a("
"),I=a("
").width("100%").height("100%").css({zIndex:310,position:"absolute",overflow:"hidden"}),J=a("
").width("100%").height("100%").css("zIndex",320),K=a("
").css({position:"absolute",zIndex:600}).dblclick(function(){var a=_.getFixed();d.onDblClick.call(bs,a)}).insertBefore(D).append(I,J);B&&(H=a("").attr("src",D.attr("src")).css(z).width(E).height(F),I.append(H)),h&&K.css({overflowY:"hidden"});var L=d.boundary,M=y().width(E+L*2).height(F+L*2).css({position:"absolute",top:i(-L),left:i(-L),zIndex:290}).mousedown(w),N=d.bgColor,O=d.bgOpacity,P,Q,R,S,T,U,V=!0,W,X,Y;e=l(D);var Z=function(){function a(){var a={},b=["touchstart" 13 | ,"touchmove","touchend"],c=document.createElement("div"),d;try{for(d=0;da+f&&(f-=f+a),0>b+g&&(g-=g+b),FE&&(r=E,u=Math.abs((r-a)/f),s=k<0?b-u:u+b)):(r=c,u=l/f,s=k<0?b-u:b+u,s<0?(s=0,t=Math.abs((s-b)*f),r=j<0?a-t:t+a):s>F&&(s=F,t=Math.abs(s-b)*f,r=j<0?a-t:t+a)),r>a?(r-ah&&(r=a+h),s>b?s=b+(r-a)/f:s=b-(r-a)/f):rh&&(r=a-h),s>b?s=b+(a-r)/f:s=b-(a-r)/f),r<0?(a-=r,r=0):r>E&&(a-=r-E,r=E),s<0?(b-=s,s=0):s>F&&(b-=s-F,s=F),q(o(a,b,r,s))}function n(a){return a[0]<0&&(a[0]=0),a[1]<0&&(a[1]=0),a[0]>E&&(a[0]=E),a[1]>F&&(a[1]=F),[Math.round(a[0]),Math.round(a[1])]}function o(a,b,c,d){var e=a,f=c,g=b,h=d;return cP&&(c=d>0?a+P:a-P),Q&&Math.abs 15 | (f)>Q&&(e=f>0?b+Q:b-Q),S/U&&Math.abs(f)0?b+S/U:b-S/U),R/T&&Math.abs(d)0?a+R/T:a-R/T),a<0&&(c-=a,a-=a),b<0&&(e-=b,b-=b),c<0&&(a-=c,c-=c),e<0&&(b-=e,e-=e),c>E&&(g=c-E,a-=g,c-=g),e>F&&(g=e-F,b-=g,e-=g),a>E&&(g=a-F,e-=g,b-=g),b>F&&(g=b-F,e-=g,b-=g),q(o(a,b,c,e))}function q(a){return{x:a[0],y:a[1],x2:a[2],y2:a[3],w:a[2]-a[0],h:a[3]-a[1]}}var a=0,b=0,c=0,e=0,f,g;return{flipCoords:o,setPressed:h,setCurrent:i,getOffset:j,moveOffset:k,getCorner:l,getFixed:m}}(),ba=function(){function f(a,b){e.left.css({height:i(b)}),e.right.css({height:i(b)})}function g(){return h(_.getFixed())}function h(a){e.top.css({left:i(a.x),width:i(a.w),height:i(a.y)}),e.bottom.css({top:i(a.y2),left:i(a.x),width:i(a.w),height:i(F-a.y2)}),e.right.css({left:i(a.x2),width:i(E-a.x2)}),e.left.css({width:i(a.x)})}function j(){return a("
").css({position:"absolute",backgroundColor:d.shadeColor||d.bgColor}).appendTo(c)}function k(){b||(b=!0,c.insertBefore(D),g(),bb.setBgOpacity(1,0,1),H.hide(),l(d.shadeColor||d.bgColor,1),bb. 16 | isAwake()?n(d.bgOpacity,1):n(1,1))}function l(a,b){bq(p(),a,b)}function m(){b&&(c.remove(),H.show(),b=!1,bb.isAwake()?bb.setBgOpacity(d.bgOpacity,1,1):(bb.setBgOpacity(1,1,1),bb.disableHandles()),bq(G,0,1))}function n(a,e){b&&(d.bgFade&&!e?c.animate({opacity:1-a},{queue:!1,duration:d.fadeTime}):c.css({opacity:1-a}))}function o(){d.shade?k():m(),bb.isAwake()&&n(d.bgOpacity)}function p(){return c.children()}var b=!1,c=a("
").css({position:"absolute",zIndex:240,opacity:0}),e={top:j(),left:j().height(F),right:j().height(F),bottom:j()};return{update:g,updateRaw:h,getShades:p,setBgColor:l,enable:k,disable:m,resize:f,refresh:o,opacity:n}}(),bb=function(){function k(b){var c=a("
").css({position:"absolute",opacity:d.borderOpacity}).addClass(j(b));return I.append(c),c}function l(b,c){var d=a("
").mousedown(s(b)).css({cursor:b+"-resize",position:"absolute",zIndex:c}).addClass("ord-"+b);return Z.support&&d.bind("touchstart.jcrop",Z.createDragger(b)),J.append(d),d}function m(a){var b=d.handleSize,e=l(a,c++ 17 | ).css({opacity:d.handleOpacity}).addClass(j("handle"));return b&&e.width(b).height(b),e}function n(a){return l(a,c++).addClass("jcrop-dragbar")}function o(a){var b;for(b=0;b').css({position:"fixed",left:"-120px",width:"12px"}).addClass("jcrop-keymgr"),c=a("
").css({position:"absolute",overflow:"hidden"}).append(b);return d.keySupport&&(b.keydown(i).blur(f),h||!d.fixedSupport?(b.css({position:"absolute",left:"-20px"}),c.append(b).insertBefore(D)):b.insertBefore(D)),{watchKeys:e}}();Z.support&&M.bind("touchstart.jcrop",Z.newSelection),J.hide(),br(!0);var bs={setImage:bp,animateTo:bf,setSelect:bg,setOptions:bk,tellSelect:bi,tellScaled:bj,setClass:be,disable:bl,enable:bm,cancel:bn,release:bb.release,destroy:bo,focus:bd.watchKeys,getBounds:function(){return[E*T,F*U]},getWidgetSize:function(){return[E,F]},getScaleFactor:function(){return[T,U]},getOptions:function(){return d},ui:{holder:G,selection:K}};return g&&G.bind("selectstart",function(){return!1}),A.data("Jcrop",bs),bs},a.fn.Jcrop=function(b,c){var d;return this.each(function(){if(a(this).data("Jcrop")){if( 21 | b==="api")return a(this).data("Jcrop");a(this).data("Jcrop").setOptions(b)}else this.tagName=="IMG"?a.Jcrop.Loader(this,function(){a(this).css({display:"block",visibility:"hidden"}),d=a.Jcrop(this,b),a.isFunction(c)&&c.call(d)}):(a(this).css({display:"block",visibility:"hidden"}),d=a.Jcrop(this,b),a.isFunction(c)&&c.call(d))}),this},a.Jcrop.Loader=function(b,c,d){function g(){f.complete?(e.unbind(".jcloader"),a.isFunction(c)&&c.call(f)):window.setTimeout(g,50)}var e=a(b),f=e[0];e.bind("load.jcloader",g).bind("error.jcloader",function(b){e.unbind(".jcloader"),a.isFunction(d)&&d.call(f)}),f.complete&&a.isFunction(c)&&(e.unbind(".jcloader"),c.call(f))},a.Jcrop.defaults={allowSelect:!0,allowMove:!0,allowResize:!0,trackDocument:!0,baseClass:"jcrop",addClass:null,bgColor:"black",bgOpacity:.6,bgFade:!1,borderOpacity:.4,handleOpacity:.5,handleSize:null,aspectRatio:0,keySupport:!0,createHandles:["n","s","e","w","nw","ne","se","sw"],createDragbars:["n","s","e","w"],createBorders:["n","s","e","w"],drawBorders:!0,dragEdges 22 | :!0,fixedSupport:!0,touchSupport:null,shade:null,boxWidth:0,boxHeight:0,boundary:2,fadeTime:400,animationDelay:20,swingSpeed:3,minSelect:[0,0],maxSize:[0,0],minSize:[0,0],onChange:function(){},onSelect:function(){},onDblClick:function(){},onRelease:function(){}}})(jQuery); -------------------------------------------------------------------------------- /app/static/js/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.4.1 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD 11 | define(['jquery'], factory); 12 | } else if (typeof exports === 'object') { 13 | // CommonJS 14 | factory(require('jquery')); 15 | } else { 16 | // Browser globals 17 | factory(jQuery); 18 | } 19 | }(function ($) { 20 | 21 | var pluses = /\+/g; 22 | 23 | function encode(s) { 24 | return config.raw ? s : encodeURIComponent(s); 25 | } 26 | 27 | function decode(s) { 28 | return config.raw ? s : decodeURIComponent(s); 29 | } 30 | 31 | function stringifyCookieValue(value) { 32 | return encode(config.json ? JSON.stringify(value) : String(value)); 33 | } 34 | 35 | function parseCookieValue(s) { 36 | if (s.indexOf('"') === 0) { 37 | // This is a quoted cookie as according to RFC2068, unescape... 38 | s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); 39 | } 40 | 41 | try { 42 | // Replace server-side written pluses with spaces. 43 | // If we can't decode the cookie, ignore it, it's unusable. 44 | // If we can't parse the cookie, ignore it, it's unusable. 45 | s = decodeURIComponent(s.replace(pluses, ' ')); 46 | return config.json ? JSON.parse(s) : s; 47 | } catch(e) {} 48 | } 49 | 50 | function read(s, converter) { 51 | var value = config.raw ? s : parseCookieValue(s); 52 | return $.isFunction(converter) ? converter(value) : value; 53 | } 54 | 55 | var config = $.cookie = function (key, value, options) { 56 | 57 | // Write 58 | 59 | if (value !== undefined && !$.isFunction(value)) { 60 | options = $.extend({}, config.defaults, options); 61 | 62 | if (typeof options.expires === 'number') { 63 | var days = options.expires, t = options.expires = new Date(); 64 | t.setTime(+t + days * 864e+5); 65 | } 66 | 67 | return (document.cookie = [ 68 | encode(key), '=', stringifyCookieValue(value), 69 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 70 | options.path ? '; path=' + options.path : '', 71 | options.domain ? '; domain=' + options.domain : '', 72 | options.secure ? '; secure' : '' 73 | ].join('')); 74 | } 75 | 76 | // Read 77 | 78 | var result = key ? undefined : {}; 79 | 80 | // To prevent the for loop in the first place assign an empty array 81 | // in case there are no cookies at all. Also prevents odd result when 82 | // calling $.cookie(). 83 | var cookies = document.cookie ? document.cookie.split('; ') : []; 84 | 85 | for (var i = 0, l = cookies.length; i < l; i++) { 86 | var parts = cookies[i].split('='); 87 | var name = decode(parts.shift()); 88 | var cookie = parts.join('='); 89 | 90 | if (key && key === name) { 91 | // If second argument (value) is a function it's a converter... 92 | result = read(cookie, value); 93 | break; 94 | } 95 | 96 | // Prevent storing a cookie that we couldn't decode. 97 | if (!key && (cookie = read(cookie)) !== undefined) { 98 | result[name] = cookie; 99 | } 100 | } 101 | 102 | return result; 103 | }; 104 | 105 | config.defaults = {}; 106 | 107 | $.removeCookie = function (key, options) { 108 | if ($.cookie(key) === undefined) { 109 | return false; 110 | } 111 | 112 | // Must not alter options, thus extending a fresh object... 113 | $.cookie(key, '', $.extend({}, options, { expires: -1 })); 114 | return !$.cookie(key); 115 | }; 116 | 117 | })); 118 | -------------------------------------------------------------------------------- /app/static/js/jquery_from.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Form Plugin 3 | * version: 3.51.0-2014.06.20 4 | * Requires jQuery v1.5 or later 5 | * Copyright (c) 2014 M. Alsup 6 | * Examples and documentation at: http://malsup.com/jquery/form/ 7 | * Project repository: https://github.com/malsup/form 8 | * Dual licensed under the MIT and GPL licenses. 9 | * https://github.com/malsup/form#copyright-and-license 10 | */ 11 | !function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):e("undefined"!=typeof jQuery?jQuery:window.Zepto)}(function(e){"use strict";function t(t){var r=t.data;t.isDefaultPrevented()||(t.preventDefault(),e(t.target).ajaxSubmit(r))}function r(t){var r=t.target,a=e(r);if(!a.is("[type=submit],[type=image]")){var n=a.closest("[type=submit]");if(0===n.length)return;r=n[0]}var i=this;if(i.clk=r,"image"==r.type)if(void 0!==t.offsetX)i.clk_x=t.offsetX,i.clk_y=t.offsetY;else if("function"==typeof e.fn.offset){var o=a.offset();i.clk_x=t.pageX-o.left,i.clk_y=t.pageY-o.top}else i.clk_x=t.pageX-r.offsetLeft,i.clk_y=t.pageY-r.offsetTop;setTimeout(function(){i.clk=i.clk_x=i.clk_y=null},100)}function a(){if(e.fn.ajaxSubmit.debug){var t="[jquery.form] "+Array.prototype.join.call(arguments,"");window.console&&window.console.log?window.console.log(t):window.opera&&window.opera.postError&&window.opera.postError(t)}}var n={};n.fileapi=void 0!==e("").get(0).files,n.formdata=void 0!==window.FormData;var i=!!e.fn.prop;e.fn.attr2=function(){if(!i)return this.attr.apply(this,arguments);var e=this.prop.apply(this,arguments);return e&&e.jquery||"string"==typeof e?e:this.attr.apply(this,arguments)},e.fn.ajaxSubmit=function(t){function r(r){var a,n,i=e.param(r,t.traditional).split("&"),o=i.length,s=[];for(a=0;o>a;a++)i[a]=i[a].replace(/\+/g," "),n=i[a].split("="),s.push([decodeURIComponent(n[0]),decodeURIComponent(n[1])]);return s}function o(a){for(var n=new FormData,i=0;i').val(m.extraData[d].value).appendTo(w)[0]:e('').val(m.extraData[d]).appendTo(w)[0]);m.iframeTarget||v.appendTo("body"),g.attachEvent?g.attachEvent("onload",s):g.addEventListener("load",s,!1),setTimeout(t,15);try{w.submit()}catch(h){var x=document.createElement("form").submit;x.apply(w)}}finally{w.setAttribute("action",i),w.setAttribute("enctype",c),r?w.setAttribute("target",r):f.removeAttr("target"),e(l).remove()}}function s(t){if(!x.aborted&&!F){if(M=n(g),M||(a("cannot access response document"),t=k),t===D&&x)return x.abort("timeout"),void S.reject(x,"timeout");if(t==k&&x)return x.abort("server abort"),void S.reject(x,"error","server abort");if(M&&M.location.href!=m.iframeSrc||T){g.detachEvent?g.detachEvent("onload",s):g.removeEventListener("load",s,!1);var r,i="success";try{if(T)throw"timeout";var o="xml"==m.dataType||M.XMLDocument||e.isXMLDoc(M);if(a("isXml="+o),!o&&window.opera&&(null===M.body||!M.body.innerHTML)&&--O)return a("requeing onLoad callback, DOM not available"),void setTimeout(s,250);var u=M.body?M.body:M.documentElement;x.responseText=u?u.innerHTML:null,x.responseXML=M.XMLDocument?M.XMLDocument:M,o&&(m.dataType="xml"),x.getResponseHeader=function(e){var t={"content-type":m.dataType};return t[e.toLowerCase()]},u&&(x.status=Number(u.getAttribute("status"))||x.status,x.statusText=u.getAttribute("statusText")||x.statusText);var c=(m.dataType||"").toLowerCase(),l=/(json|script|text)/.test(c);if(l||m.textarea){var f=M.getElementsByTagName("textarea")[0];if(f)x.responseText=f.value,x.status=Number(f.getAttribute("status"))||x.status,x.statusText=f.getAttribute("statusText")||x.statusText;else if(l){var p=M.getElementsByTagName("pre")[0],h=M.getElementsByTagName("body")[0];p?x.responseText=p.textContent?p.textContent:p.innerText:h&&(x.responseText=h.textContent?h.textContent:h.innerText)}}else"xml"==c&&!x.responseXML&&x.responseText&&(x.responseXML=X(x.responseText));try{E=_(x,c,m)}catch(y){i="parsererror",x.error=r=y||i}}catch(y){a("error caught: ",y),i="error",x.error=r=y||i}x.aborted&&(a("upload aborted"),i=null),x.status&&(i=x.status>=200&&x.status<300||304===x.status?"success":"error"),"success"===i?(m.success&&m.success.call(m.context,E,"success",x),S.resolve(x.responseText,"success",x),d&&e.event.trigger("ajaxSuccess",[x,m])):i&&(void 0===r&&(r=x.statusText),m.error&&m.error.call(m.context,x,i,r),S.reject(x,"error",r),d&&e.event.trigger("ajaxError",[x,m,r])),d&&e.event.trigger("ajaxComplete",[x,m]),d&&!--e.active&&e.event.trigger("ajaxStop"),m.complete&&m.complete.call(m.context,x,i),F=!0,m.timeout&&clearTimeout(j),setTimeout(function(){m.iframeTarget?v.attr("src",m.iframeSrc):v.remove(),x.responseXML=null},100)}}}var c,l,m,d,p,v,g,x,y,b,T,j,w=f[0],S=e.Deferred();if(S.abort=function(e){x.abort(e)},r)for(l=0;l'),v.css({position:"absolute",top:"-1000px",left:"-1000px"})),g=v[0],x={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(t){var r="timeout"===t?"timeout":"aborted";a("aborting upload... "+r),this.aborted=1;try{g.contentWindow.document.execCommand&&g.contentWindow.document.execCommand("Stop")}catch(n){}v.attr("src",m.iframeSrc),x.error=r,m.error&&m.error.call(m.context,x,r,t),d&&e.event.trigger("ajaxError",[x,m,r]),m.complete&&m.complete.call(m.context,x,r)}},d=m.global,d&&0===e.active++&&e.event.trigger("ajaxStart"),d&&e.event.trigger("ajaxSend",[x,m]),m.beforeSend&&m.beforeSend.call(m.context,x,m)===!1)return m.global&&e.active--,S.reject(),S;if(x.aborted)return S.reject(),S;y=w.clk,y&&(b=y.name,b&&!y.disabled&&(m.extraData=m.extraData||{},m.extraData[b]=y.value,"image"==y.type&&(m.extraData[b+".x"]=w.clk_x,m.extraData[b+".y"]=w.clk_y)));var D=1,k=2,A=e("meta[name=csrf-token]").attr("content"),L=e("meta[name=csrf-param]").attr("content");L&&A&&(m.extraData=m.extraData||{},m.extraData[L]=A),m.forceSync?o():setTimeout(o,10);var E,M,F,O=50,X=e.parseXML||function(e,t){return window.ActiveXObject?(t=new ActiveXObject("Microsoft.XMLDOM"),t.async="false",t.loadXML(e)):t=(new DOMParser).parseFromString(e,"text/xml"),t&&t.documentElement&&"parsererror"!=t.documentElement.nodeName?t:null},C=e.parseJSON||function(e){return window.eval("("+e+")")},_=function(t,r,a){var n=t.getResponseHeader("content-type")||"",i="xml"===r||!r&&n.indexOf("xml")>=0,o=i?t.responseXML:t.responseText;return i&&"parsererror"===o.documentElement.nodeName&&e.error&&e.error("parsererror"),a&&a.dataFilter&&(o=a.dataFilter(o,r)),"string"==typeof o&&("json"===r||!r&&n.indexOf("json")>=0?o=C(o):("script"===r||!r&&n.indexOf("javascript")>=0)&&e.globalEval(o)),o};return S}if(!this.length)return a("ajaxSubmit: skipping submit process - no element selected"),this;var u,c,l,f=this;"function"==typeof t?t={success:t}:void 0===t&&(t={}),u=t.type||this.attr2("method"),c=t.url||this.attr2("action"),l="string"==typeof c?e.trim(c):"",l=l||window.location.href||"",l&&(l=(l.match(/^([^#]+)/)||[])[1]),t=e.extend(!0,{url:l,success:e.ajaxSettings.success,type:u||e.ajaxSettings.type,iframeSrc:/^https/i.test(window.location.href||"")?"javascript:false":"about:blank"},t);var m={};if(this.trigger("form-pre-serialize",[this,t,m]),m.veto)return a("ajaxSubmit: submit vetoed via form-pre-serialize trigger"),this;if(t.beforeSerialize&&t.beforeSerialize(this,t)===!1)return a("ajaxSubmit: submit aborted via beforeSerialize callback"),this;var d=t.traditional;void 0===d&&(d=e.ajaxSettings.traditional);var p,h=[],v=this.formToArray(t.semantic,h);if(t.data&&(t.extraData=t.data,p=e.param(t.data,d)),t.beforeSubmit&&t.beforeSubmit(v,this,t)===!1)return a("ajaxSubmit: submit aborted via beforeSubmit callback"),this;if(this.trigger("form-submit-validate",[v,this,t,m]),m.veto)return a("ajaxSubmit: submit vetoed via form-submit-validate trigger"),this;var g=e.param(v,d);p&&(g=g?g+"&"+p:p),"GET"==t.type.toUpperCase()?(t.url+=(t.url.indexOf("?")>=0?"&":"?")+g,t.data=null):t.data=g;var x=[];if(t.resetForm&&x.push(function(){f.resetForm()}),t.clearForm&&x.push(function(){f.clearForm(t.includeHidden)}),!t.dataType&&t.target){var y=t.success||function(){};x.push(function(r){var a=t.replaceTarget?"replaceWith":"html";e(t.target)[a](r).each(y,arguments)})}else t.success&&x.push(t.success);if(t.success=function(e,r,a){for(var n=t.context||this,i=0,o=x.length;o>i;i++)x[i].apply(n,[e,r,a||f,f])},t.error){var b=t.error;t.error=function(e,r,a){var n=t.context||this;b.apply(n,[e,r,a,f])}}if(t.complete){var T=t.complete;t.complete=function(e,r){var a=t.context||this;T.apply(a,[e,r,f])}}var j=e("input[type=file]:enabled",this).filter(function(){return""!==e(this).val()}),w=j.length>0,S="multipart/form-data",D=f.attr("enctype")==S||f.attr("encoding")==S,k=n.fileapi&&n.formdata;a("fileAPI :"+k);var A,L=(w||D)&&!k;t.iframe!==!1&&(t.iframe||L)?t.closeKeepAlive?e.get(t.closeKeepAlive,function(){A=s(v)}):A=s(v):A=(w||D)&&k?o(v):e.ajax(t),f.removeData("jqxhr").data("jqxhr",A);for(var E=0;Ec;c++)if(d=u[c],f=d.name,f&&!d.disabled)if(t&&o.clk&&"image"==d.type)o.clk==d&&(a.push({name:f,value:e(d).val(),type:d.type}),a.push({name:f+".x",value:o.clk_x},{name:f+".y",value:o.clk_y}));else if(m=e.fieldValue(d,!0),m&&m.constructor==Array)for(r&&r.push(d),l=0,h=m.length;h>l;l++)a.push({name:f,value:m[l]});else if(n.fileapi&&"file"==d.type){r&&r.push(d);var v=d.files;if(v.length)for(l=0;li;i++)r.push({name:a,value:n[i]});else null!==n&&"undefined"!=typeof n&&r.push({name:this.name,value:n})}}),e.param(r)},e.fn.fieldValue=function(t){for(var r=[],a=0,n=this.length;n>a;a++){var i=this[a],o=e.fieldValue(i,t);null===o||"undefined"==typeof o||o.constructor==Array&&!o.length||(o.constructor==Array?e.merge(r,o):r.push(o))}return r},e.fieldValue=function(t,r){var a=t.name,n=t.type,i=t.tagName.toLowerCase();if(void 0===r&&(r=!0),r&&(!a||t.disabled||"reset"==n||"button"==n||("checkbox"==n||"radio"==n)&&!t.checked||("submit"==n||"image"==n)&&t.form&&t.form.clk!=t||"select"==i&&-1==t.selectedIndex))return null;if("select"==i){var o=t.selectedIndex;if(0>o)return null;for(var s=[],u=t.options,c="select-one"==n,l=c?o+1:u.length,f=c?o:0;l>f;f++){var m=u[f];if(m.selected){var d=m.value;if(d||(d=m.attributes&&m.attributes.value&&!m.attributes.value.specified?m.text:m.value),c)return d;s.push(d)}}return s}return e(t).val()},e.fn.clearForm=function(t){return this.each(function(){e("input,select,textarea",this).clearFields(t)})},e.fn.clearFields=e.fn.clearInputs=function(t){var r=/^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i;return this.each(function(){var a=this.type,n=this.tagName.toLowerCase();r.test(a)||"textarea"==n?this.value="":"checkbox"==a||"radio"==a?this.checked=!1:"select"==n?this.selectedIndex=-1:"file"==a?/MSIE/.test(navigator.userAgent)?e(this).replaceWith(e(this).clone(!0)):e(this).val(""):t&&(t===!0&&/hidden/.test(a)||"string"==typeof t&&e(this).is(t))&&(this.value="")})},e.fn.resetForm=function(){return this.each(function(){("function"==typeof this.reset||"object"==typeof this.reset&&!this.reset.nodeType)&&this.reset()})},e.fn.enable=function(e){return void 0===e&&(e=!0),this.each(function(){this.disabled=!e})},e.fn.selected=function(t){return void 0===t&&(t=!0),this.each(function(){var r=this.type;if("checkbox"==r||"radio"==r)this.checked=t;else if("option"==this.tagName.toLowerCase()){var a=e(this).parent("select");t&&a[0]&&"select-one"==a[0].type&&a.find("option").selected(!1),this.selected=t}})},e.fn.ajaxSubmit.debug=!1}); -------------------------------------------------------------------------------- /app/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | from flask import Blueprint 3 | 4 | tasks = Blueprint('tasks', __name__) 5 | 6 | from . import celerymail -------------------------------------------------------------------------------- /app/tasks/celerymail.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | from .. import celery, mail, create_app, db 3 | from ..models import User, Post, Webpush 4 | 5 | @celery.task 6 | def send_async_email(msg): 7 | app = create_app('default') 8 | with app.app_context(): 9 | mail.send(msg) 10 | 11 | @celery.task 12 | def send_async_webpush(username,postid): 13 | app = create_app('default') 14 | with app.app_context(): 15 | user = User.query.filter_by(username=username).first() 16 | post = Post.query.get(postid) 17 | followers = user.followers 18 | for follower in followers: 19 | if follower.follower != user: 20 | webpush = Webpush(sendto=follower.follower,author=user,post_id=post.id) 21 | 22 | -------------------------------------------------------------------------------- /app/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Fly - Forbidden{% endblock %} 4 | 5 | {% block page_content %} 6 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /app/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Fly - Page Not Found{% endblock %} 4 | 5 | {% block page_content %} 6 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /app/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Fly - Internal Server Error{% endblock %} 4 | 5 | {% block page_content %} 6 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /app/templates/_comments.html: -------------------------------------------------------------------------------- 1 |
    2 | {% for comment in comments %} 3 |
  • 4 |
    5 | 6 | 7 | 8 |
    9 |
    10 |
    {{ comment.timestamp }}
    11 | 12 |
    13 | {% if comment.disabled %} 14 |

    This comment has been disabled by a moderator.

    15 | {% endif %} 16 | {% if moderate or not comment.disabled %} 17 | {% if comment.body_html %} 18 | {{ comment.body_html | safe }} 19 | {% else %} 20 | {{ comment.body }} 21 | {% endif %} 22 | {% endif %} 23 |
    24 |
    25 |
  • 26 | {% endfor %} 27 |
28 | -------------------------------------------------------------------------------- /app/templates/_comments_moderate.html: -------------------------------------------------------------------------------- 1 | {% if current_user.is_authenticated %} 2 |
    3 | {% for comment in comments %} 4 |
  • 5 |
    6 | 7 | 8 | 9 |
    10 |
    11 |
    {{ comment.timestamp }}
    12 | 13 |
    14 | 评论了博客: 15 | {{comment.post.head}} 16 |
    17 |
    18 | {% if comment.disabled %} 19 |

    This comment has been disabled by a moderator.

    20 | {% endif %} 21 | {% if moderate or not comment.disabled %} 22 | {% if comment.body_html %} 23 | {{ comment.body_html | safe }} 24 | {% else %} 25 | {{ comment.body }} 26 | {% endif %} 27 | {% endif %} 28 |
    29 | {% if moderate %} 30 |
    31 | {% if comment.disabled %} 32 | 恢复正常 33 | {% else %} 34 | 屏蔽 35 | {% endif %} 36 | {% endif %} 37 |
    38 |
  • 39 | {% endfor %} 40 |
41 | {% endif %} 42 | -------------------------------------------------------------------------------- /app/templates/_index_posts.html: -------------------------------------------------------------------------------- 1 | 61 | -------------------------------------------------------------------------------- /app/templates/_macros.html: -------------------------------------------------------------------------------- 1 | {% macro pagination_widget(pagination, endpoint, fragment='') %} 2 |
    3 | 4 | 5 | « 6 | 7 | 8 | {% for p in pagination.iter_pages() %} 9 | {% if p %} 10 | {% if p == pagination.page %} 11 |
  • 12 | {{ p }} 13 |
  • 14 | {% else %} 15 |
  • 16 | {{ p }} 17 |
  • 18 | {% endif %} 19 | {% else %} 20 |
  • 21 | {% endif %} 22 | {% endfor %} 23 | 24 | 25 | » 26 | 27 | 28 |
29 | {% endmacro %} 30 | -------------------------------------------------------------------------------- /app/templates/_message.html: -------------------------------------------------------------------------------- 1 | {% if current_user.is_authenticated %} 2 |
    3 | {% for message in messages %} 4 | {% if message.sendto == current_user %} 5 |
  1. 6 |
    7 | 8 | 9 | 10 |
    11 |
    12 |
    {{ message.timestamp}}
    13 | 14 |
    15 | {{ message.body }} 16 |
    17 | {% if message %} 18 |
    19 | {% if not message.confirmed %} 20 | 标记为已读 21 | {% else %} 22 | 标记为未读 23 | {% endif %} 24 | 删除 25 | 回复 26 | 27 | 28 | {% endif %} 29 |
    30 |
  2. 31 | {% endif %} 32 | {% endfor %} 33 |
34 | {% endif %} -------------------------------------------------------------------------------- /app/templates/_notice.html: -------------------------------------------------------------------------------- 1 | {% if current_user.is_authenticated %} 2 |
    3 | {% for comment in comments %} 4 | {% if comment.post.author == current_user %} 5 |
  • 6 |
    7 | 8 | 9 | 10 |
    11 |
    12 |
    {{ comment.timestamp }}
    13 | 14 |
    15 | 评论了博客: 16 | {{comment.post.head}} 17 |
    18 |
    19 | {% if comment.disabled %} 20 |

    This comment has been disabled by a moderator.

    21 | {% endif %} 22 | {% if moderate or not comment.disabled %} 23 | {% if comment.body_html %} 24 | {{ comment.body_html | safe }} 25 | {% else %} 26 | {{ comment.body }} 27 | {% endif %} 28 | {% endif %} 29 |
    30 | {% if message %} 31 |
    32 | {% if not comment.confirmed %} 33 | 标记为已读 34 | {% else %} 35 | 标记为未读 36 | {% endif %} 37 | {% endif %} 38 |
    39 |
  • 40 | {% endif %} 41 | {% endfor %} 42 |
43 | {% endif %} -------------------------------------------------------------------------------- /app/templates/_posts.html: -------------------------------------------------------------------------------- 1 | 68 | -------------------------------------------------------------------------------- /app/templates/_userbase.html: -------------------------------------------------------------------------------- 1 | 2 | 52 | -------------------------------------------------------------------------------- /app/templates/_webpush.html: -------------------------------------------------------------------------------- 1 | {% if current_user.is_authenticated %} 2 | 55 | {% endif %} -------------------------------------------------------------------------------- /app/templates/aboutme.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly{% endblock %} 6 | 7 | 8 | {% block topimg %} 9 |
10 |

Get source code

11 |
愚者不努力,懒人盼巅峰
12 |
13 | {% endblock %} 14 | 15 | 16 | {% block page_content %} 17 |

关于小站

18 |

Email:ifwenvlook@163.com  19 | 20 | 21 | 22 | 23 | 24 |

25 | 26 |

27 | 从前有座山,山上有座庙。
云白松青泉水澄,方丈持经童儿笑。
摩诃般若妙。

山中无日月,佛门无白皂。
岂闻天下俱兴兵,为得人前身后名,血流漂杵不留行。
佛前香零丁。

天下苍生苦,眼下清泪咸,忽有一日到山前。
北坡军威盛,东村号炮鸣。
不尽杀戮与血腥。
童儿直发抖,方丈只念经。
我佛慈悲若有情,但求战事顷刻停,阿弥陀佛念万遍,已见长庚星。
喊声才宁息。

月明星稀三更天,山门之外人马疲,血衣将军滚下马,马头轻顶撞柴扉。
方丈忙惊起,披衣开门扉,虽恐血光脏我佛,不忍将军命西归。
两日将军醒,五日将军归,七日庙宇人马围。
一问曰不知,二问曰不明,六名僧众皆圆寂。
佛倒庙塌去。

日落月升,沧海桑田。
天子巡游至山前。
文官赞山美,武将夸水甜。
丞相铺纸画山景,圣上亲题辞。
辞曰:
从前有座山,山上有座庙…… 28 |

29 | {% endblock %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/templates/admin/addadmin.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/edit.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly-添加管理员{% endblock %} 6 | 7 | 8 | {% block page_content %} 9 | 12 |
13 | {{ wtf.quick_form(form) }} 14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/admin/addcategory.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/edit.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly-创建新分类{% endblock %} 6 | 7 | 8 | {% block page_content %} 9 | 12 |
13 | {{ wtf.quick_form(form) }} 14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/admin/adduser.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/edit.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly-添加普通用户{% endblock %} 6 | 7 | 8 | {% block page_content %} 9 | 12 |
13 | {{ wtf.quick_form(form) }} 14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/admin/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly-admin后台{% endblock %} 6 | 7 | {% block navbar %} 8 |
9 | 15 | 25 |
26 | 27 | 79 | {% endblock %} 80 | 81 | 82 | {% block contenthead %} 83 | {{ super() }} 84 |
85 | 89 | 93 |
94 | {% endblock %} 95 | 96 | 97 | {% block page_content %} 98 | 99 | {{super()}} 100 | 101 | {% if current_user.is_administrator() %} 102 |

{{ current_user.username }},欢迎来到Blog后台 {{ moment(g.current_time).format('LLL') }}

103 | 104 | 109 |
110 | 后台管理员 111 | 所有用户 112 | 文章管理 113 | 分类管理 114 | 评论管理 115 |
116 |
117 | 118 | 119 |

后台管理员列表 120 |  添加管理员 121 | 122 |

123 |
124 |
125 |
126 | 127 | 128 | 129 | 130 | 131 | 132 | {% for user in admins %} 133 | 134 | {% if user.username %} 135 | 136 | 137 | {% if user != current_user %} 138 | 142 | {% else %} 143 | 147 | {% endif %} 148 | {% endif %} 149 | 150 | {% endfor %} 151 |
管理员注册时间操作
{{ user.username }}{{ moment(user.member_since).format('L') }} 139 | 降为普通用户 140 | 141 | 144 | 花式作死 145 | 146 |
152 |
153 | 154 |
155 |
156 | 157 | {% if pagination %} 158 | 161 | {% endif %} 162 | 163 | {%else %} 164 |

你没有权限进入后台,请联系网站管理员:ifwenvlook@163.com

165 | {% endif %} 166 | 167 | {% endblock %} 168 | 169 | -------------------------------------------------------------------------------- /app/templates/admin/editcategory.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/edit.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly-admin文章管理{% endblock %} 6 | 7 | 8 | {% block page_content %} 9 | 10 | 11 | 12 |

{{ current_user.username }},欢迎来到Blog后台 {{ moment(g.current_time).format('LLL') }}

13 | 14 | 19 | 26 |
27 | 28 |

分类列表 29 |  创建新分类 30 |

31 | 32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% for category in categorys %} 43 | 44 | 45 | 46 | 47 | 55 | 56 | {% endfor %} 57 |
ID名称文章总数操作
{{ category.id }}{{ category.name }}{{category.posts.count()}} 48 | 49 | 编辑 50 |    51 | 52 | 删除 53 | 54 |
58 |
59 | 60 |
61 |
62 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/admin/editcomment.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/edit.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly-admin评论管理{% endblock %} 6 | 7 | 8 | {% block page_content %} 9 | 10 |

{{ current_user.username }},欢迎来到Blog后台 {{ moment(g.current_time).format('LLL') }}

11 | 12 | 17 | 24 |
25 | 26 |

评论管理

27 | 28 | 29 | {% if current_user.is_authenticated %} 30 |
    31 | {% for comment in comments %} 32 |
  • 33 |
    34 | 35 | 36 | 37 |
    38 |
    39 |
    {{ comment.timestamp }}
    40 | 41 |
    42 | 评论了博客: 43 | {{comment.post.head}} 44 |
    45 |
    46 | {% if comment.disabled %} 47 |

    This comment has been disabled by a moderator.

    48 | {% endif %} 49 | {% if not comment.disabled %} 50 | {% if comment.body_html %} 51 | {{ comment.body_html | safe }} 52 | {% else %} 53 | {{ comment.body }} 54 | {% endif %} 55 | {% endif %} 56 |
    57 |
    58 | {% if comment.disabled %} 59 | 恢复正常 60 | {% else %} 61 | 屏蔽 62 | {% endif %} 63 | 删除 64 |
    65 |
  • 66 | {% endfor %} 67 |
68 | {% endif %} 69 | 70 | {% if pagination %} 71 | 74 | {% endif %} 75 | 76 | 77 | {% endblock %} 78 | -------------------------------------------------------------------------------- /app/templates/admin/editpost.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/edit.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly-admin文章管理{% endblock %} 6 | 7 | 8 | {% block page_content %} 9 | 10 | 11 | 12 |

{{ current_user.username }},欢迎来到Blog后台 {{ moment(g.current_time).format('LLL') }}

13 | 14 | 19 | 26 |
27 | 28 |

文章列表

29 | 30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {% for post in posts %} 42 | 43 | 44 | 45 | 46 | 53 | 62 | 63 | {% endfor %} 64 |
作者标题发表时间操作正文
{{ post.author.username }}{{ post.head }}{{ moment(post.timestamp).format('L') }} 47 | 编辑 48 |    49 | 50 | 删除 51 | 52 | 54 | 55 | {% if post.body[30] %} 56 | {{ post.body[0:30] }}.... 57 | {% else %} 58 | {{ post.body}} 59 | {% endif %} 60 | 61 |
65 |
66 | 67 |
68 |
69 | {% if pagination %} 70 | 73 | {% endif %} 74 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/admin/edituser.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/edit.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly-admin用户管理{% endblock %} 6 | 7 | 8 | 9 | {% block page_content %} 10 |

{{ current_user.username }},欢迎来到Blog后台 {{ moment(g.current_time).format('LLL') }}

11 | 12 | 17 | 24 |
25 | 26 | 27 |

用户列表 28 |  添加普通用户 29 | 30 |

31 |
32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | {% for user in users %} 41 | 42 | {% if user.username %} 43 | 44 | 45 | {% if not user.is_administrator() %} 46 | 50 | {% elif user.is_administrator() and user != current_user %} 51 | 55 | {% else %} 56 | 60 | {% endif %} 61 | {% endif %} 62 | 63 | {% endfor %} 64 |
用户注册时间操作
{{ user.username }}{{ moment(user.member_since).format('L') }} 47 | 删除用户 48 | 49 | 52 | 删除Admin 53 | 54 | 57 | 花式作死 58 | 59 |
65 |
66 | 67 |
68 |
69 | {% if pagination %} 70 | 71 | 74 | {% endif %} 75 | 76 | {% endblock %} 77 | 78 | -------------------------------------------------------------------------------- /app/templates/auth/change_email.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Fly - 修改电子邮箱{% endblock %} 5 | 6 | {% block page_content %} 7 | 12 |
13 | {{ wtf.quick_form(form) }} 14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/auth/change_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Fly - 账户设置{% endblock %} 5 | 6 | {% block page_content %} 7 | 13 | 14 |
15 | {{ wtf.quick_form(form) }} 16 |
17 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/auth/change_userset.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Fly - 账户设置{% endblock %} 5 | 6 | {% block page_content %} 7 | 13 | 14 |
15 | {{ wtf.quick_form(form) }} 16 |
17 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/auth/email/change_email.html: -------------------------------------------------------------------------------- 1 |

Dear {{ user.username }},

2 |

To confirm your new email address click here.

3 |

Alternatively, you can paste the following link in your browser's address bar:

4 |

{{ url_for('auth.change_email', token=token, _external=True) }}

5 |

Sincerely,

6 |

The Flasky Team

7 |

Note: replies to this email address are not monitored.

8 | -------------------------------------------------------------------------------- /app/templates/auth/email/change_email.txt: -------------------------------------------------------------------------------- 1 | Dear {{ user.username }}, 2 | 3 | To confirm your new email address click on the following link: 4 | 5 | {{ url_for('auth.change_email', token=token, _external=True) }} 6 | 7 | Sincerely, 8 | 9 | The Flasky Team 10 | 11 | Note: replies to this email address are not monitored. 12 | -------------------------------------------------------------------------------- /app/templates/auth/email/confirm.html: -------------------------------------------------------------------------------- 1 |

Dear {{ user.username }},

2 |

Welcome to Flasky!

3 |

To confirm your account please click here.

4 |

Alternatively, you can paste the following link in your browser's address bar:

5 |

{{ url_for('auth.confirm', token=token, _external=True) }}

6 |

Sincerely,

7 |

The Flasky Team

8 |

Note: replies to this email address are not monitored.

9 | -------------------------------------------------------------------------------- /app/templates/auth/email/confirm.txt: -------------------------------------------------------------------------------- 1 | Dear {{ user.username }}, 2 | 3 | Welcome to Flasky! 4 | 5 | To confirm your account please click on the following link: 6 | 7 | {{ url_for('auth.confirm', token=token, _external=True) }} 8 | 9 | Sincerely, 10 | 11 | The Flasky Team 12 | 13 | Note: replies to this email address are not monitored. 14 | -------------------------------------------------------------------------------- /app/templates/auth/email/reset_password.html: -------------------------------------------------------------------------------- 1 |

Dear {{ user.username }},

2 |

To reset your password click here.

3 |

Alternatively, you can paste the following link in your browser's address bar:

4 |

{{ url_for('auth.password_reset', token=token, _external=True) }}

5 |

If you have not requested a password reset simply ignore this message.

6 |

Sincerely,

7 |

The Flasky Team

8 |

Note: replies to this email address are not monitored.

9 | -------------------------------------------------------------------------------- /app/templates/auth/email/reset_password.txt: -------------------------------------------------------------------------------- 1 | Dear {{ user.username }}, 2 | 3 | To reset your password click on the following link: 4 | 5 | {{ url_for('auth.password_reset', token=token, _external=True) }} 6 | 7 | If you have not requested a password reset simply ignore this message. 8 | 9 | Sincerely, 10 | 11 | The Flasky Team 12 | 13 | Note: replies to this email address are not monitored. 14 | -------------------------------------------------------------------------------- /app/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Fly - 登录{% endblock %} 5 | 6 | 7 | {% block contenthead %} 8 | {{ super() }} 9 |
10 | 14 | 18 |
19 | {% endblock %} 20 | 21 | {% block page_content %} 22 | 25 |
26 | {{ wtf.quick_form(form) }} 27 |
28 |

忘记密码? 找回密码.

29 |

新用户? 注册.

30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /app/templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Fly - 注册{% endblock %} 5 | 6 | {% block contenthead %} 7 | {{ super() }} 8 |
9 | 13 | 17 |
18 | {% endblock %} 19 | 20 | {% block page_content %} 21 | 24 |
25 | {{ wtf.quick_form(form) }} 26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /app/templates/auth/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Flasky - Password Reset{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 |
11 | {{ wtf.quick_form(form) }} 12 |
13 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/auth/unconfirmed.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Flasky - 确认你的账户{% endblock %} 4 | 5 | {% block page_content %} 6 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /app/templates/bootstrap_base.html: -------------------------------------------------------------------------------- 1 | {% block doc %} 2 | 3 | 4 | {% block html %} 5 | 6 | {% block head %} 7 | {% block title %}{% endblock title %} 8 | 9 | {% block metas %} 10 | 11 | {% endblock metas %} 12 | 13 | {% block styles %} 14 | 15 | 16 | {% endblock styles %} 17 | {% endblock head %} 18 | 19 | 20 |
21 | {% block body %} 22 | {% block navbar %} 23 | {% endblock navbar %} 24 | 25 | 26 | 27 |
28 | {% block content %} 29 | {% block topimg %} 30 | {% endblock topimg %}. 31 | {% block contenthead %} 32 | {% endblock contenthead %} 33 |
34 | {% block page_content %} 35 | {% endblock page_content %} 36 |
37 | {% endblock content %} 38 |
39 | 40 | {% block footandrightnavbar %} 41 | {% endblock footandrightnavbar %} 42 | 43 | {% block scripts %} 44 | {% endblock scripts %} 45 | 46 | {% endblock body %} 47 |
48 | 49 | {% endblock html %} 50 | 51 | {% endblock doc %} 52 | -------------------------------------------------------------------------------- /app/templates/category.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Fly - {{ category.name }}{% endblock %} 5 | 6 | {% block page_content %} 7 |

{{ category.name }}

8 | {% include '_index_posts.html' %} 9 | {% if pagination %} 10 | 13 | {% endif %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /app/templates/edit_post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Fly - 编辑博客{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 |
11 | {{ wtf.quick_form(form) }} 12 |
13 | {% endblock %} 14 | 15 | {% block scripts %} 16 | {{ super() }} 17 | {{ pagedown.include_pagedown() }} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /app/templates/edit_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Fly - 编辑个人资料{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 |
11 | {{ wtf.quick_form(form) }} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /app/templates/error_page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Fly - {{ code }}: {{ name }}{% endblock %} 4 | 5 | {% block page_content %} 6 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /app/templates/followers.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Fly - {{ title }} {{ user.username }}{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 | 11 | 12 | {% for follow in follows %} 13 | {% if follow.user != user %} 14 | 15 | 21 | 22 | 23 | {% endif %} 24 | {% endfor %} 25 |
UserSince
16 | 17 | 18 | {{ follow.user.username }} 19 | 20 | {{ moment(follow.timestamp).format('L') }}
26 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% block content %} 13 | 14 | {% block topimg %} 15 |
16 |

Get source code

17 |
愚者不努力,懒人盼巅峰
18 |
19 | {% endblock %} 20 | 21 | {% block contenthead %} 22 | {{ super() }} 23 |
24 | 28 | 32 |
33 | {% endblock %} 34 | 35 | {% block page_content %} 36 |
37 | 44 | {% include '_index_posts.html' %} 45 |
46 | {% if pagination %} 47 | 50 | {% endif %} 51 | {% endblock %} 52 | 53 | {% endblock %} 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/templates/mail/new_user.html: -------------------------------------------------------------------------------- 1 | User {{ user.username }} has joined. 2 | -------------------------------------------------------------------------------- /app/templates/mail/new_user.txt: -------------------------------------------------------------------------------- 1 | User {{ user.username }} has joined. 2 | -------------------------------------------------------------------------------- /app/templates/moderate.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Fly - 评论管理{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 | {% set moderate = True %} 11 | {% include '_comments_moderate.html' %} 12 | {% if pagination %} 13 | 16 | {% endif %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /app/templates/post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly - 博客{% endblock %} 6 | 7 | {% block page_content %} 8 | {% include '_posts.html' %} 9 |

评论

10 | {% if current_user.can(Permission.COMMENT) %} 11 |
12 | {{ wtf.quick_form(form) }} 13 |
14 | {% endif %} 15 | {% include '_comments.html' %} 16 | {% if pagination %} 17 | 20 | {% endif %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /app/templates/search_results.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Fly - 搜索{% endblock %} 4 | 5 | {% block page_content %} 6 | 7 | {% if posts[0] %} 8 |

"{{query}}" 的搜索结果:

9 | {% include '_index_posts.html' %} 10 | {% else %} 11 |

未找到"{{query}}"的搜索结果,请精简关键字,关键字中不要带有符号、空格等等

12 | {% endif %} 13 | 14 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/sendmessage.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Flasky - 私信{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 |
11 | {{ wtf.quick_form(form) }} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /app/templates/showmessage.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Fly - 查看私信{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 | {% set message = True %} 11 | {% include '_message.html' %} 12 | {% if pagination %} 13 | 16 | {% endif %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /app/templates/shownotice.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Fly - 查看通知{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 | {% set message = True %} 11 | {% include '_notice.html' %} 12 | {% if pagination %} 13 | 16 | {% endif %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /app/templates/unconfirmed.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Flasky - Confirm your accont{% endblock %} 4 | 5 | {% block page_content %} 6 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /app/templates/user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Fly - {{ user.username }}{% endblock %} 5 | 6 | {% block page_content %} 7 | {% include '_userbase.html' %} 8 | 13 |
14 | {% include '_posts.html' %} 15 | {% if pagination %} 16 | 19 | {% endif %} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /app/templates/user_comments.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Fly - 用户评论{% endblock %} 5 | 6 | {% block page_content %} 7 | {% include '_userbase.html' %} 8 | 9 | 14 |
15 | 16 | {% if current_user != user%} 17 |
    18 | {% for comment in comments %} 19 | {% if comment.author == user %} 20 |
  • 21 |
    22 | 23 | 24 | 25 |
    26 |
    27 |
    {{ moment(comment.timestamp).fromNow() }}
    28 | 29 |
    30 | 评论了博客: 31 | {{comment.post.head}} 32 |
    33 |
    34 | {% if comment.disabled %} 35 |

    This comment has been disabled by a moderator.

    36 | {% endif %} 37 | {% if moderate or not comment.disabled %} 38 | {% if comment.body_html %} 39 | {{ comment.body_html | safe }} 40 | {% else %} 41 | {{ comment.body }} 42 | {% endif %} 43 | {% endif %} 44 |
    45 |
    46 |
  • 47 | {% endif %} 48 | {% endfor %} 49 | 50 | {% elif current_user==user %} 51 |
      52 | {% for comment in comments %} 53 | {% if comment.author == current_user %} 54 |
    • 55 |
      56 | 57 | 58 | 59 |
      60 |
      61 |
      {{ moment(comment.timestamp).format('L') }}
      62 | 63 |
      64 | 我评论了博客: 65 | {{comment.post.head}} 66 |
      67 |
      68 | {% if comment.disabled %} 69 |

      This comment has been disabled by a moderator.

      70 | {% endif %} 71 | {% if moderate or not comment.disabled %} 72 | {% if comment.body_html %} 73 | {{ comment.body_html | safe }} 74 | {% else %} 75 | {{ comment.body }} 76 | {% endif %} 77 | {% endif %} 78 |
      79 | 删除 80 |
      81 |
      82 |
      83 |
    • 84 | {% endif %} 85 | {% endfor %} 86 |
    87 | {% endif %} 88 | 89 | {% if pagination %} 90 | 93 | {% endif %} 94 | {% endblock %} 95 | -------------------------------------------------------------------------------- /app/templates/user_showwebpush.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Fly - 查看订阅{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 | {% set webpush = True %} 11 | {% include '_webpush.html' %} 12 | {% if pagination %} 13 | 16 | {% endif %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /app/templates/user_starposts.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Fly - {{ user.username }} {{ title }} {% endblock %} 5 | 6 | {% block page_content %} 7 | {% include '_userbase.html' %} 8 | 13 |
    14 | 15 |
    16 |
    17 | 18 | 28 |
    29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% for post in posts %} 38 | 39 | 40 | 41 | 42 | 53 | 62 | 63 | {% endfor %} 64 |
    作者标题收藏时间操作正文
    {{ post.author.username }}{{ post.head }}{{ moment(user.startimestamp(post)).format('L') }} 43 | {% if current_user == user %} 44 | 45 | 删除 46 | 47 | {% else %} 48 | 49 | 无权限 50 | 51 | {% endif %} 52 | 54 | 55 | {% if post.body[30] %} 56 | {{ post.body[0:30] }}.... 57 | {% else %} 58 | {{ post.body}} 59 | {% endif %} 60 | 61 |
    65 |
    66 | 67 |
    68 |
    69 | {% endblock %} 70 | -------------------------------------------------------------------------------- /app/templates/video.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% import "bootstrap/wtf.html" as wtf %} 4 | {% import "_macros.html" as macros %} 5 | 6 | {% block title %}Fly - video{% endblock %} 7 | 8 | {% block page_content %} 9 | 10 | {{super()}} 11 | 12 |
    13 |

    视频来自Bilibili

    14 |
    15 | 16 | 12分钟看完90万字科幻神作《三体》 17 |
    18 | 19 |

    20 | 21 | 22 | 三体·黑暗森林同人短片 23 |
    24 | 25 | 26 | 27 |

    28 | 妹子没吃药打架子鼓感觉自己萌萌哒 29 |
    30 | 31 | 32 | 33 |

    34 | 银河系富二代在地球开车违章 35 |
    36 | 37 | 38 |

    39 | papi纪念莎士比亚400周年特别视频 40 |
    41 | 42 | 43 | 44 | 45 | 46 | 47 |
    48 | 49 | {% endblock %} 50 | 51 | {% block scripts %} 52 | {{ super() }} 53 | {{ pagedown.include_pagedown() }} 54 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/writepost.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Fly{% endblock %} 6 | 7 | {% block page_content %} 8 | 9 | {{super()}} 10 | 11 | 12 |
    13 | {{ form.hidden_tag() }} 14 | 15 | 16 |
    17 |
    18 | 23 |
    24 |
    25 | 26 |
    27 |
    28 | 29 |
    30 | 31 |
    32 | 33 |
    34 | 35 |
    36 |
    37 | 38 |
    39 | 40 |
    41 | 42 | 43 | 44 | 64 | 65 | {% endblock %} 66 | {% block scripts %} 67 | {{ super() }} 68 | {{ pagedown.include_pagedown() }} 69 | {% endblock %} -------------------------------------------------------------------------------- /celery_worker.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | import os 3 | from app import celery, create_app 4 | from celery import platforms 5 | 6 | 7 | platforms.C_FORCE_ROOT = True #加上这一行 8 | app = create_app('default') 9 | app.app_context().push() 10 | #start celery worker -A celery_worker.celery -l INFO /celery worker -A celery_worker.celery --loglevel=info -------------------------------------------------------------------------------- /centos_config/centos_requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.6.2 2 | amqp==1.4.9 3 | anyjson==0.3.3 4 | billiard==3.3.0.23 5 | bleach==1.4 6 | blinker==1.3 7 | celery==3.1.23 8 | colorama==0.2.7 9 | coverage==3.7.1 10 | defusedxml==0.4.1 11 | Flask==0.10.1 12 | Flask-Bootstrap==3.0.3.1 13 | Flask-HTTPAuth==2.7.0 14 | Flask-Login==0.3.1 15 | Flask-Mail==0.9.0 16 | Flask-Migrate==1.1.0 17 | Flask-Moment==0.2.1 18 | Flask-PageDown==0.1.4 19 | Flask-Script==0.6.6 20 | Flask-SQLAlchemy==1.0 21 | Flask-WTF==0.9.4 22 | ForgeryPy==0.1 23 | html5lib==1.0b3 24 | httpie==0.7.2 25 | itsdangerous==0.23 26 | Jinja2==2.7.1 27 | kombu==3.0.35 28 | Mako==0.9.1 29 | Markdown==2.3.1 30 | MarkupSafe==0.18 31 | mysql-connector-python==2.1.3 32 | Pygments==1.6 33 | pytz==2016.4 34 | redis==2.10.5 35 | requests==2.1.0 36 | selenium==2.45.0 37 | six==1.4.1 38 | SQLAlchemy==1.0.12 39 | uWSGI==2.0.12 40 | Werkzeug==0.11.9 41 | WTForms==1.0.5 42 | -------------------------------------------------------------------------------- /centos_config/nginx_default.conf: -------------------------------------------------------------------------------- 1 | # 2 | # The default server 3 | # 4 | server { 5 | listen 80 ; 6 | server_name 120.76.133.73; 7 | 8 | #charset koi8-r; 9 | 10 | #access_log logs/host.access.log main; 11 | 12 | # Load configuration files for the default server block. 13 | include /etc/nginx/default.d/*.conf; 14 | 15 | location / { 16 | root /usr/share/nginx/html; 17 | index index.html index.htm; 18 | include uwsgi_params; 19 | uwsgi_pass 127.0.0.1:7115; 20 | uwsgi_param UWSGI_SCRIPT manage:app; 21 | uwsgi_param UWSGI_CHDIR /home/blog; 22 | } 23 | 24 | error_page 404 /404.html; 25 | location = /404.html { 26 | root /usr/share/nginx/html; 27 | } 28 | 29 | # redirect server error pages to the static page /50x.html 30 | # 31 | error_page 500 502 503 504 /50x.html; 32 | location = /50x.html { 33 | root /usr/share/nginx/html; 34 | } 35 | 36 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 37 | # 38 | #location ~ \.php$ { 39 | # proxy_pass http://127.0.0.1; 40 | #} 41 | 42 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 43 | # 44 | #location ~ \.php$ { 45 | # root html; 46 | # fastcgi_pass 127.0.0.1:9000; 47 | # fastcgi_index index.php; 48 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 49 | # include fastcgi_params; 50 | #} 51 | 52 | # deny access to .htaccess files, if Apache's document root 53 | # concurs with nginx's one 54 | # 55 | #location ~ /\.ht { 56 | # deny all; 57 | #} 58 | } 59 | 60 | 61 | -------------------------------------------------------------------------------- /centos_config/nolog_restartweb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | service mysqld restart 4 | service nginx restart 5 | service redis stop 6 | sleep 3 7 | service redis start 8 | killall -9 uwsgi 9 | cd /home/blog 10 | celery worker -A celery_worker.celery -l INFO & 11 | uwsgi /home/blog/config.ini 12 | -------------------------------------------------------------------------------- /centos_config/redis: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #chkconfig: 345 86 14 3 | #description: Startup and shutdown script for Redis 4 | 5 | PROGDIR=/usr/local/bin #安装路径 6 | PROGNAME=redis-server 7 | DAEMON=$PROGDIR/$PROGNAME 8 | CONFIG=/root/redis-stable/redis.conf 9 | PIDFILE=/var/run/redis.pid 10 | DESC="redis daemon" 11 | SCRIPTNAME=/etc/rc.d/init.d/redis 12 | 13 | start() 14 | { 15 | if test -x $DAEMON 16 | then 17 | echo -e "Starting $DESC: $PROGNAME" 18 | if $DAEMON $CONFIG 19 | then 20 | echo -e "OK" 21 | else 22 | echo -e "failed" 23 | fi 24 | else 25 | echo -e "Couldn't find Redis Server ($DAEMON)" 26 | fi 27 | } 28 | 29 | stop() 30 | { 31 | if test -e $PIDFILE 32 | then 33 | echo -e "Stopping $DESC: $PROGNAME" 34 | if kill `cat $PIDFILE` 35 | then 36 | echo -e "OK" 37 | else 38 | echo -e "failed" 39 | fi 40 | else 41 | echo -e "No Redis Server ($DAEMON) running" 42 | fi 43 | } 44 | 45 | restart() 46 | { 47 | echo -e "Restarting $DESC: $PROGNAME" 48 | stop 49 | start 50 | } 51 | 52 | list() 53 | { 54 | ps aux | grep $PROGNAME 55 | } 56 | 57 | case $1 in 58 | start) 59 | start 60 | ;; 61 | stop) 62 | stop 63 | ;; 64 | restart) 65 | restart 66 | ;; 67 | list) 68 | list 69 | ;; 70 | 71 | *) 72 | echo "Usage: $SCRIPTNAME {start|stop|restart|list}" >&2 73 | exit 1 74 | ;; 75 | esac 76 | exit 0 77 | -------------------------------------------------------------------------------- /centos_config/restartweb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | service mysqld restart 4 | service nginx restart 5 | service redis stop 6 | killall -9 uwsgi 7 | uwsgi /home/blog/config.ini -d /home/log/uwsgi.log 8 | killall -9 celery 9 | sleep 3 10 | service redis start 11 | cd /home/blog 12 | celery worker -A celery_worker.celery -l INFO & 13 | -------------------------------------------------------------------------------- /centos_config/stopweb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | service mysqld stop 4 | service redis stop 5 | service nginx stop 6 | killall -9 uwsgi 7 | killall -9 celery 8 | -------------------------------------------------------------------------------- /centos_config/win_requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Flask-Bootstrap==3.0.3.1 3 | Flask-HTTPAuth==2.7.0 4 | Flask-Login==0.3.1 5 | Flask-Mail==0.9.1 6 | Flask-Migrate==1.1.0 7 | Flask-Moment==0.2.1 8 | Flask-PageDown==0.1.4 9 | Flask-SQLAlchemy==1.0 10 | Flask-Script==0.6.6 11 | Flask-WTF==0.9.4 12 | ForgeryPy==0.1 13 | Jinja2==2.7.3 14 | Mako==0.9.1 15 | Markdown==2.3.1 16 | MarkupSafe==0.23 17 | Pygments==1.6 18 | SQLAlchemy==1.0.12 19 | WTForms==1.0.5 20 | Werkzeug==0.11.9 21 | alembic==0.6.2 22 | amqp==1.4.6 23 | anyjson==0.3.3 24 | argparse==1.2.1 25 | billiard==3.3.0.19 26 | bleach==1.4 27 | blinker==1.3 28 | celery==3.1.17 29 | celery-with-redis==3.0 30 | colorama==0.2.7 31 | coverage==3.7.1 32 | defusedxml==0.4.1 33 | html5lib==1.0b3 34 | httpie==0.7.2 35 | itsdangerous==0.24 36 | kombu==3.0.24 37 | mysql-connector-python==2.1.3 38 | pytz==2014.10 39 | redis==2.10.3 40 | requests==2.1.0 41 | selenium==2.45.0 42 | six==1.4.1 43 | virtualenv==15.0.1 44 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | 3 | socket=127.0.0.1:7115 4 | 5 | processes=4 6 | 7 | threads=2 8 | 9 | master=true 10 | 11 | pythonpath=/home/blog 12 | 13 | module= manage 14 | 15 | callable=app 16 | 17 | memory-report=true 18 | 19 | 20 | stats=127.0.0.1:9191 21 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | basedir = os.path.abspath(os.path.dirname(__file__)) 3 | 4 | 5 | class Config: 6 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' 7 | SSL_DISABLE = False 8 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 9 | SQLALCHEMY_RECORD_QUERIES = True 10 | MAIL_SERVER = 'smtp.qq.com' 11 | MAIL_PORT = 587 12 | MAIL_USE_TLS = True 13 | MAIL_USERNAME = 'XXX' 14 | MAIL_PASSWORD = 'XXX' 15 | MAIL_DEFAULT_SENDER = 'XXX' 16 | FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]' 17 | FLASKY_MAIL_SENDER = 'Flasky Admin ' 18 | FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN') 19 | FLASKY_POSTS_PER_PAGE = 20 20 | FLASKY_FOLLOWERS_PER_PAGE = 50 21 | FLASKY_COMMENTS_PER_PAGE = 30 22 | FLASKY_SLOW_DB_QUERY_TIME=0.5 23 | CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' 24 | CELERY_BROKER_URL = 'redis://localhost:6379/0' 25 | 26 | 27 | 28 | @staticmethod 29 | def init_app(app): 30 | pass 31 | 32 | 33 | class DevelopmentConfig(Config): 34 | DEBUG = True 35 | SQLALCHEMY_DATABASE_URI = 'mysql://root:password@localhost/flaskdev' 36 | # os.environ.get('DEV_DATABASE_URL') or \ 37 | # 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite') 38 | 39 | 40 | class TestingConfig(Config): 41 | TESTING = True 42 | SQLALCHEMY_DATABASE_URI = 'mysql://root:password@localhost/flasktest' 43 | # os.environ.get('DEV_DATABASE_URL') or \ 44 | # 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite') 45 | 46 | WTF_CSRF_ENABLED = False 47 | 48 | 49 | class ProductionConfig(Config): 50 | SQLALCHEMY_DATABASE_URI = 'mysql://root:password@localhost/flask' 51 | # os.environ.get('DEV_DATABASE_URL') or \ 52 | # 'sqlite:///' + os.path.join(basedir, 'data.sqlite') 53 | 54 | 55 | @classmethod 56 | def init_app(cls, app): 57 | Config.init_app(app) 58 | 59 | # email errors to the administrators 60 | import logging 61 | from logging.handlers import SMTPHandler 62 | credentials = None 63 | secure = None 64 | if getattr(cls, 'MAIL_USERNAME', None) is not None: 65 | credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD) 66 | if getattr(cls, 'MAIL_USE_TLS', None): 67 | secure = () 68 | mail_handler = SMTPHandler( 69 | mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT), 70 | fromaddr=cls.FLASKY_MAIL_SENDER, 71 | toaddrs=[cls.FLASKY_ADMIN], 72 | subject=cls.FLASKY_MAIL_SUBJECT_PREFIX + ' Application Error', 73 | credentials=credentials, 74 | secure=secure) 75 | mail_handler.setLevel(logging.ERROR) 76 | app.logger.addHandler(mail_handler) 77 | 78 | 79 | class HerokuConfig(ProductionConfig): 80 | SSL_DISABLE = bool(os.environ.get('SSL_DISABLE')) 81 | 82 | @classmethod 83 | def init_app(cls, app): 84 | ProductionConfig.init_app(app) 85 | 86 | # handle proxy server headers 87 | from werkzeug.contrib.fixers import ProxyFix 88 | app.wsgi_app = ProxyFix(app.wsgi_app) 89 | 90 | # log to stderr 91 | import logging 92 | from logging import StreamHandler 93 | file_handler = StreamHandler() 94 | file_handler.setLevel(logging.WARNING) 95 | app.logger.addHandler(file_handler) 96 | 97 | 98 | class UnixConfig(ProductionConfig): 99 | @classmethod 100 | def init_app(cls, app): 101 | ProductionConfig.init_app(app) 102 | 103 | # log to syslog 104 | import logging 105 | from logging.handlers import SysLogHandler 106 | syslog_handler = SysLogHandler() 107 | syslog_handler.setLevel(logging.WARNING) 108 | app.logger.addHandler(syslog_handler) 109 | 110 | 111 | config = { 112 | 'development': DevelopmentConfig, 113 | 'testing': TestingConfig, 114 | 'production': ProductionConfig, 115 | 'heroku': HerokuConfig, 116 | 'unix': UnixConfig, 117 | 118 | 'default': DevelopmentConfig 119 | } 120 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | #!/usr/bin/env python 3 | import os 4 | COV = None 5 | if os.environ.get('FLASK_COVERAGE'): 6 | import coverage 7 | COV = coverage.coverage(branch=True, include='app/*') 8 | COV.start() 9 | 10 | if os.path.exists('.env'): 11 | print('Importing environment from .env...') 12 | for line in open('.env'): 13 | var = line.strip().split('=') 14 | if len(var) == 2: 15 | os.environ[var[0]] = var[1] 16 | 17 | from app import create_app, db 18 | from app.delete import deletenone 19 | from app.models import User, Follow, Role, Permission, Post, Comment, Message, Category, Star, Webpush 20 | from flask.ext.script import Manager, Shell 21 | from flask.ext.migrate import Migrate, MigrateCommand 22 | 23 | app = create_app(os.getenv('FLASK_CONFIG') or 'default') 24 | manager = Manager(app) 25 | migrate = Migrate(app, db) 26 | 27 | 28 | def make_shell_context(): 29 | return dict(app=app, db=db, User=User, Follow=Follow, Role=Role, 30 | Permission=Permission, Post=Post, Comment=Comment,Message=Message,Category=Category,Star=Star,Webpush=Webpush,deletenone=deletenone) 31 | manager.add_command("shell", Shell(make_context=make_shell_context)) 32 | manager.add_command('db', MigrateCommand) 33 | 34 | 35 | 36 | 37 | @manager.command 38 | def test(coverage=False): 39 | """Run the unit tests.""" 40 | if coverage and not os.environ.get('FLASK_COVERAGE'): 41 | import sys 42 | os.environ['FLASK_COVERAGE'] = '1' 43 | os.execvp(sys.executable, [sys.executable] + sys.argv) 44 | import unittest 45 | tests = unittest.TestLoader().discover('tests') 46 | unittest.TextTestRunner(verbosity=2).run(tests) 47 | if COV: 48 | COV.stop() 49 | COV.save() 50 | print('Coverage Summary:') 51 | COV.report() 52 | basedir = os.path.abspath(os.path.dirname(__file__)) 53 | covdir = os.path.join(basedir, 'tmp/coverage') 54 | COV.html_report(directory=covdir) 55 | print('HTML version: file://%s/index.html' % covdir) 56 | COV.erase() 57 | 58 | 59 | @manager.command 60 | def profile(length=25, profile_dir=None): 61 | """Start the application under the code profiler.""" 62 | from werkzeug.contrib.profiler import ProfilerMiddleware 63 | app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length], 64 | profile_dir=profile_dir) 65 | app.run() 66 | 67 | 68 | #测试数据库初始化 69 | @manager.command 70 | def datainit(): 71 | from app.models import Role,User,Post,Category 72 | print ("Category init") 73 | Category.insert_categorys() 74 | print ("Role init") 75 | User.add_self_follows() 76 | Role.insert_roles() 77 | print ("User and Post generate") 78 | User.generate_fake(100) 79 | Post.generate_fake(100) 80 | wen=User.query.filter_by(username='wen').first() 81 | if not wen: 82 | print ("make wen in admin") 83 | wen=User(username='wen',email='xxx@mail.com',password='XXX',confirmed=True) 84 | wen.role=Role.query.filter_by(permissions=0xff).first() 85 | db.session.add(wen) 86 | db.session.commit() 87 | else : 88 | print ("User(wen) already in data") 89 | print ("all_data readly now") 90 | 91 | 92 | @manager.command 93 | def deploy(): 94 | """Run deployment tasks.""" 95 | from flask.ext.migrate import upgrade 96 | from app.models import Role, User 97 | 98 | # migrate database to latest revision 99 | upgrade() 100 | 101 | # create user roles 102 | Role.insert_roles() 103 | 104 | # create self-follows for all users 105 | User.add_self_follows() 106 | 107 | 108 | if __name__ == '__main__': 109 | manager.run() 110 | -------------------------------------------------------------------------------- /requirements/common.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Flask-Bootstrap==3.0.3.1 3 | Flask-HTTPAuth==2.7.0 4 | Flask-Login==0.3.1 5 | Flask-Mail==0.9.0 6 | Flask-Migrate==1.1.0 7 | Flask-Moment==0.2.1 8 | Flask-PageDown==0.1.4 9 | Flask-SQLAlchemy==1.0 10 | Flask-Script==0.6.6 11 | Flask-WTF==0.9.4 12 | Jinja2==2.7.1 13 | Mako==0.9.1 14 | Markdown==2.3.1 15 | MarkupSafe==0.18 16 | SQLAlchemy==1.0.12 17 | WTForms==1.0.5 18 | Werkzeug==0.10.4 19 | alembic==0.6.2 20 | bleach==1.4.0 21 | blinker==1.3 22 | html5lib==1.0b3 23 | itsdangerous==0.23 24 | six==1.4.1 25 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Flask-Bootstrap==3.0.3.1 3 | Flask-HTTPAuth==2.7.0 4 | Flask-Login==0.3.1 5 | Flask-Mail==0.9.1 6 | Flask-Migrate==1.1.0 7 | Flask-Moment==0.2.1 8 | Flask-PageDown==0.1.4 9 | Flask-SQLAlchemy==1.0 10 | Flask-Script==0.6.6 11 | Flask-WTF==0.9.4 12 | ForgeryPy==0.1 13 | Jinja2==2.7.3 14 | Mako==0.9.1 15 | Markdown==2.3.1 16 | MarkupSafe==0.23 17 | Pygments==1.6 18 | SQLAlchemy==1.0.12 19 | WTForms==1.0.5 20 | Werkzeug==0.9.6 21 | alembic==0.6.2 22 | amqp==1.4.6 23 | anyjson==0.3.3 24 | argparse==1.2.1 25 | billiard==3.3.0.19 26 | bleach==1.4 27 | blinker==1.3 28 | celery==3.1.17 29 | celery-with-redis==3.0 30 | colorama==0.2.7 31 | coverage==3.7.1 32 | defusedxml==0.4.1 33 | html5lib==1.0b3 34 | httpie==0.7.2 35 | itsdangerous==0.24 36 | kombu==3.0.24 37 | 38 | pytz==2014.10 39 | redis==2.10.3 40 | requests==2.1.0 41 | selenium==2.45.0 42 | six==1.4.1 43 | virtualenv==15.0.1 44 | -------------------------------------------------------------------------------- /requirements/heroku.txt: -------------------------------------------------------------------------------- 1 | -r prod.txt 2 | Flask-SSLify==0.1.4 3 | gunicorn==18.0 4 | psycopg2==2.5.1 5 | -------------------------------------------------------------------------------- /requirements/prod.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifwenvlook/blog/e897e55a71bfa351fc8c73a9ab699e691cbead55/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import re 4 | from base64 import b64encode 5 | from flask import url_for 6 | from app import create_app, db 7 | from app.models import User, Role, Post, Comment 8 | 9 | 10 | class APITestCase(unittest.TestCase): 11 | def setUp(self): 12 | self.app = create_app('testing') 13 | self.app_context = self.app.app_context() 14 | self.app_context.push() 15 | db.create_all() 16 | Role.insert_roles() 17 | self.client = self.app.test_client() 18 | 19 | def tearDown(self): 20 | db.session.remove() 21 | db.drop_all() 22 | self.app_context.pop() 23 | 24 | def get_api_headers(self, username, password): 25 | return { 26 | 'Authorization': 'Basic ' + b64encode( 27 | (username + ':' + password).encode('utf-8')).decode('utf-8'), 28 | 'Accept': 'application/json', 29 | 'Content-Type': 'application/json' 30 | } 31 | 32 | def test_404(self): 33 | response = self.client.get( 34 | '/wrong/url', 35 | headers=self.get_api_headers('email', 'password')) 36 | self.assertTrue(response.status_code == 404) 37 | json_response = json.loads(response.data.decode('utf-8')) 38 | self.assertTrue(json_response['error'] == 'not found') 39 | 40 | def test_no_auth(self): 41 | response = self.client.get(url_for('api.get_posts'), 42 | content_type='application/json') 43 | self.assertTrue(response.status_code == 200) 44 | 45 | def test_bad_auth(self): 46 | # add a user 47 | r = Role.query.filter_by(name='User').first() 48 | self.assertIsNotNone(r) 49 | u = User(email='john@example.com', password='cat', confirmed=True, 50 | role=r) 51 | db.session.add(u) 52 | db.session.commit() 53 | 54 | # authenticate with bad password 55 | response = self.client.get( 56 | url_for('api.get_posts'), 57 | headers=self.get_api_headers('john@example.com', 'dog')) 58 | self.assertTrue(response.status_code == 401) 59 | 60 | def test_token_auth(self): 61 | # add a user 62 | r = Role.query.filter_by(name='User').first() 63 | self.assertIsNotNone(r) 64 | u = User(email='john@example.com', password='cat', confirmed=True, 65 | role=r) 66 | db.session.add(u) 67 | db.session.commit() 68 | 69 | # issue a request with a bad token 70 | response = self.client.get( 71 | url_for('api.get_posts'), 72 | headers=self.get_api_headers('bad-token', '')) 73 | self.assertTrue(response.status_code == 401) 74 | 75 | # get a token 76 | response = self.client.get( 77 | url_for('api.get_token'), 78 | headers=self.get_api_headers('john@example.com', 'cat')) 79 | self.assertTrue(response.status_code == 200) 80 | json_response = json.loads(response.data.decode('utf-8')) 81 | self.assertIsNotNone(json_response.get('token')) 82 | token = json_response['token'] 83 | 84 | # issue a request with the token 85 | response = self.client.get( 86 | url_for('api.get_posts'), 87 | headers=self.get_api_headers(token, '')) 88 | self.assertTrue(response.status_code == 200) 89 | 90 | def test_anonymous(self): 91 | response = self.client.get( 92 | url_for('api.get_posts'), 93 | headers=self.get_api_headers('', '')) 94 | self.assertTrue(response.status_code == 200) 95 | 96 | def test_unconfirmed_account(self): 97 | # add an unconfirmed user 98 | r = Role.query.filter_by(name='User').first() 99 | self.assertIsNotNone(r) 100 | u = User(email='john@example.com', password='cat', confirmed=False, 101 | role=r) 102 | db.session.add(u) 103 | db.session.commit() 104 | 105 | # get list of posts with the unconfirmed account 106 | response = self.client.get( 107 | url_for('api.get_posts'), 108 | headers=self.get_api_headers('john@example.com', 'cat')) 109 | self.assertTrue(response.status_code == 403) 110 | 111 | def test_posts(self): 112 | # add a user 113 | r = Role.query.filter_by(name='User').first() 114 | self.assertIsNotNone(r) 115 | u = User(email='john@example.com', password='cat', confirmed=True, 116 | role=r) 117 | db.session.add(u) 118 | db.session.commit() 119 | 120 | # write an empty post 121 | response = self.client.post( 122 | url_for('api.new_post'), 123 | headers=self.get_api_headers('john@example.com', 'cat'), 124 | data=json.dumps({'body': ''})) 125 | self.assertTrue(response.status_code == 400) 126 | 127 | # write a post 128 | response = self.client.post( 129 | url_for('api.new_post'), 130 | headers=self.get_api_headers('john@example.com', 'cat'), 131 | data=json.dumps({'body': 'body of the *blog* post'})) 132 | self.assertTrue(response.status_code == 201) 133 | url = response.headers.get('Location') 134 | self.assertIsNotNone(url) 135 | 136 | # get the new post 137 | response = self.client.get( 138 | url, 139 | headers=self.get_api_headers('john@example.com', 'cat')) 140 | self.assertTrue(response.status_code == 200) 141 | json_response = json.loads(response.data.decode('utf-8')) 142 | self.assertTrue(json_response['url'] == url) 143 | self.assertTrue(json_response['body'] == 'body of the *blog* post') 144 | self.assertTrue(json_response['body_html'] == 145 | '

    body of the blog post

    ') 146 | json_post = json_response 147 | 148 | # get the post from the user 149 | response = self.client.get( 150 | url_for('api.get_user_posts', id=u.id), 151 | headers=self.get_api_headers('john@example.com', 'cat')) 152 | self.assertTrue(response.status_code == 200) 153 | json_response = json.loads(response.data.decode('utf-8')) 154 | self.assertIsNotNone(json_response.get('posts')) 155 | self.assertTrue(json_response.get('count', 0) == 1) 156 | self.assertTrue(json_response['posts'][0] == json_post) 157 | 158 | # get the post from the user as a follower 159 | response = self.client.get( 160 | url_for('api.get_user_followed_posts', id=u.id), 161 | headers=self.get_api_headers('john@example.com', 'cat')) 162 | self.assertTrue(response.status_code == 200) 163 | json_response = json.loads(response.data.decode('utf-8')) 164 | self.assertIsNotNone(json_response.get('posts')) 165 | self.assertTrue(json_response.get('count', 0) == 1) 166 | self.assertTrue(json_response['posts'][0] == json_post) 167 | 168 | # edit post 169 | response = self.client.put( 170 | url, 171 | headers=self.get_api_headers('john@example.com', 'cat'), 172 | data=json.dumps({'body': 'updated body'})) 173 | self.assertTrue(response.status_code == 200) 174 | json_response = json.loads(response.data.decode('utf-8')) 175 | self.assertTrue(json_response['url'] == url) 176 | self.assertTrue(json_response['body'] == 'updated body') 177 | self.assertTrue(json_response['body_html'] == '

    updated body

    ') 178 | 179 | def test_users(self): 180 | # add two users 181 | r = Role.query.filter_by(name='User').first() 182 | self.assertIsNotNone(r) 183 | u1 = User(email='john@example.com', username='john', 184 | password='cat', confirmed=True, role=r) 185 | u2 = User(email='susan@example.com', username='susan', 186 | password='dog', confirmed=True, role=r) 187 | db.session.add_all([u1, u2]) 188 | db.session.commit() 189 | 190 | # get users 191 | response = self.client.get( 192 | url_for('api.get_user', id=u1.id), 193 | headers=self.get_api_headers('susan@example.com', 'dog')) 194 | self.assertTrue(response.status_code == 200) 195 | json_response = json.loads(response.data.decode('utf-8')) 196 | self.assertTrue(json_response['username'] == 'john') 197 | response = self.client.get( 198 | url_for('api.get_user', id=u2.id), 199 | headers=self.get_api_headers('susan@example.com', 'dog')) 200 | self.assertTrue(response.status_code == 200) 201 | json_response = json.loads(response.data.decode('utf-8')) 202 | self.assertTrue(json_response['username'] == 'susan') 203 | 204 | def test_comments(self): 205 | # add two users 206 | r = Role.query.filter_by(name='User').first() 207 | self.assertIsNotNone(r) 208 | u1 = User(email='john@example.com', username='john', 209 | password='cat', confirmed=True, role=r) 210 | u2 = User(email='susan@example.com', username='susan', 211 | password='dog', confirmed=True, role=r) 212 | db.session.add_all([u1, u2]) 213 | db.session.commit() 214 | 215 | # add a post 216 | post = Post(body='body of the post', author=u1) 217 | db.session.add(post) 218 | db.session.commit() 219 | 220 | # write a comment 221 | response = self.client.post( 222 | url_for('api.new_post_comment', id=post.id), 223 | headers=self.get_api_headers('susan@example.com', 'dog'), 224 | data=json.dumps({'body': 'Good [post](http://example.com)!'})) 225 | self.assertTrue(response.status_code == 201) 226 | json_response = json.loads(response.data.decode('utf-8')) 227 | url = response.headers.get('Location') 228 | self.assertIsNotNone(url) 229 | self.assertTrue(json_response['body'] == 230 | 'Good [post](http://example.com)!') 231 | self.assertTrue( 232 | re.sub('<.*?>', '', json_response['body_html']) == 'Good post!') 233 | 234 | # get the new comment 235 | response = self.client.get( 236 | url, 237 | headers=self.get_api_headers('john@example.com', 'cat')) 238 | self.assertTrue(response.status_code == 200) 239 | json_response = json.loads(response.data.decode('utf-8')) 240 | self.assertTrue(json_response['url'] == url) 241 | self.assertTrue(json_response['body'] == 242 | 'Good [post](http://example.com)!') 243 | 244 | # add another comment 245 | comment = Comment(body='Thank you!', author=u1, post=post) 246 | db.session.add(comment) 247 | db.session.commit() 248 | 249 | # get the two comments from the post 250 | response = self.client.get( 251 | url_for('api.get_post_comments', id=post.id), 252 | headers=self.get_api_headers('susan@example.com', 'dog')) 253 | self.assertTrue(response.status_code == 200) 254 | json_response = json.loads(response.data.decode('utf-8')) 255 | self.assertIsNotNone(json_response.get('posts')) 256 | self.assertTrue(json_response.get('count', 0) == 2) 257 | 258 | # get all the comments 259 | response = self.client.get( 260 | url_for('api.get_comments', id=post.id), 261 | headers=self.get_api_headers('susan@example.com', 'dog')) 262 | self.assertTrue(response.status_code == 200) 263 | json_response = json.loads(response.data.decode('utf-8')) 264 | self.assertIsNotNone(json_response.get('posts')) 265 | self.assertTrue(json_response.get('count', 0) == 2) 266 | -------------------------------------------------------------------------------- /tests/test_basics.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from flask import current_app 3 | from app import create_app, db 4 | 5 | 6 | class BasicsTestCase(unittest.TestCase): 7 | def setUp(self): 8 | self.app = create_app('testing') 9 | self.app_context = self.app.app_context() 10 | self.app_context.push() 11 | db.create_all() 12 | 13 | def tearDown(self): 14 | db.session.remove() 15 | db.drop_all() 16 | self.app_context.pop() 17 | 18 | def test_app_exists(self): 19 | self.assertFalse(current_app is None) 20 | 21 | def test_app_is_testing(self): 22 | self.assertTrue(current_app.config['TESTING']) 23 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | from flask import url_for 4 | from app import create_app, db 5 | from app.models import User, Role 6 | 7 | class FlaskClientTestCase(unittest.TestCase): 8 | def setUp(self): 9 | self.app = create_app('testing') 10 | self.app_context = self.app.app_context() 11 | self.app_context.push() 12 | db.create_all() 13 | Role.insert_roles() 14 | self.client = self.app.test_client(use_cookies=True) 15 | 16 | def tearDown(self): 17 | db.session.remove() 18 | db.drop_all() 19 | self.app_context.pop() 20 | 21 | def test_home_page(self): 22 | response = self.client.get(url_for('main.index')) 23 | self.assertTrue(b'Stranger' in response.data) 24 | 25 | def test_register_and_login(self): 26 | # register a new account 27 | response = self.client.post(url_for('auth.register'), data={ 28 | 'email': 'john@example.com', 29 | 'username': 'john', 30 | 'password': 'cat', 31 | 'password2': 'cat' 32 | }) 33 | self.assertTrue(response.status_code == 302) 34 | 35 | # login with the new account 36 | response = self.client.post(url_for('auth.login'), data={ 37 | 'email': 'john@example.com', 38 | 'password': 'cat' 39 | }, follow_redirects=True) 40 | self.assertTrue(re.search(b'Hello,\s+john!', response.data)) 41 | self.assertTrue( 42 | b'You have not confirmed your account yet' in response.data) 43 | 44 | # send a confirmation token 45 | user = User.query.filter_by(email='john@example.com').first() 46 | token = user.generate_confirmation_token() 47 | response = self.client.get(url_for('auth.confirm', token=token), 48 | follow_redirects=True) 49 | self.assertTrue( 50 | b'You have confirmed your account' in response.data) 51 | 52 | # log out 53 | response = self.client.get(url_for('auth.logout'), follow_redirects=True) 54 | self.assertTrue(b'You have been logged out' in response.data) 55 | -------------------------------------------------------------------------------- /tests/test_selenium.py: -------------------------------------------------------------------------------- 1 | import re 2 | import threading 3 | import time 4 | import unittest 5 | from selenium import webdriver 6 | from app import create_app, db 7 | from app.models import Role, User, Post 8 | 9 | 10 | class SeleniumTestCase(unittest.TestCase): 11 | client = None 12 | 13 | @classmethod 14 | def setUpClass(cls): 15 | # start Firefox 16 | try: 17 | cls.client = webdriver.Firefox() 18 | except: 19 | pass 20 | 21 | # skip these tests if the browser could not be started 22 | if cls.client: 23 | # create the application 24 | cls.app = create_app('testing') 25 | cls.app_context = cls.app.app_context() 26 | cls.app_context.push() 27 | 28 | # suppress logging to keep unittest output clean 29 | import logging 30 | logger = logging.getLogger('werkzeug') 31 | logger.setLevel("ERROR") 32 | 33 | # create the database and populate with some fake data 34 | db.create_all() 35 | Role.insert_roles() 36 | User.generate_fake(10) 37 | Post.generate_fake(10) 38 | 39 | # add an administrator user 40 | admin_role = Role.query.filter_by(permissions=0xff).first() 41 | admin = User(email='john@example.com', 42 | username='john', password='cat', 43 | role=admin_role, confirmed=True) 44 | db.session.add(admin) 45 | db.session.commit() 46 | 47 | # start the Flask server in a thread 48 | threading.Thread(target=cls.app.run).start() 49 | 50 | # give the server a second to ensure it is up 51 | time.sleep(1) 52 | 53 | @classmethod 54 | def tearDownClass(cls): 55 | if cls.client: 56 | # stop the flask server and the browser 57 | cls.client.get('http://localhost:5000/shutdown') 58 | cls.client.close() 59 | 60 | # destroy database 61 | db.drop_all() 62 | db.session.remove() 63 | 64 | # remove application context 65 | cls.app_context.pop() 66 | 67 | def setUp(self): 68 | if not self.client: 69 | self.skipTest('Web browser not available') 70 | 71 | def tearDown(self): 72 | pass 73 | 74 | def test_admin_home_page(self): 75 | # navigate to home page 76 | self.client.get('http://localhost:5000/') 77 | self.assertTrue(re.search('Hello,\s+Stranger!', 78 | self.client.page_source)) 79 | 80 | # navigate to login page 81 | self.client.find_element_by_link_text('Log In').click() 82 | self.assertTrue('

    Login

    ' in self.client.page_source) 83 | 84 | # login 85 | self.client.find_element_by_name('email').\ 86 | send_keys('john@example.com') 87 | self.client.find_element_by_name('password').send_keys('cat') 88 | self.client.find_element_by_name('submit').click() 89 | self.assertTrue(re.search('Hello,\s+john!', self.client.page_source)) 90 | 91 | # navigate to the user's profile page 92 | self.client.find_element_by_link_text('Profile').click() 93 | self.assertTrue('

    john

    ' in self.client.page_source) 94 | -------------------------------------------------------------------------------- /tests/test_user_model.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import time 3 | from datetime import datetime 4 | from app import create_app, db 5 | from app.models import User, AnonymousUser, Role, Permission, Follow 6 | 7 | 8 | class UserModelTestCase(unittest.TestCase): 9 | def setUp(self): 10 | self.app = create_app('testing') 11 | self.app_context = self.app.app_context() 12 | self.app_context.push() 13 | db.create_all() 14 | Role.insert_roles() 15 | 16 | def tearDown(self): 17 | db.session.remove() 18 | db.drop_all() 19 | self.app_context.pop() 20 | 21 | def test_password_setter(self): 22 | u = User(password='cat') 23 | self.assertTrue(u.password_hash is not None) 24 | 25 | def test_no_password_getter(self): 26 | u = User(password='cat') 27 | with self.assertRaises(AttributeError): 28 | u.password 29 | 30 | def test_password_verification(self): 31 | u = User(password='cat') 32 | self.assertTrue(u.verify_password('cat')) 33 | self.assertFalse(u.verify_password('dog')) 34 | 35 | def test_password_salts_are_random(self): 36 | u = User(password='cat') 37 | u2 = User(password='cat') 38 | self.assertTrue(u.password_hash != u2.password_hash) 39 | 40 | def test_valid_confirmation_token(self): 41 | u = User(password='cat') 42 | db.session.add(u) 43 | db.session.commit() 44 | token = u.generate_confirmation_token() 45 | self.assertTrue(u.confirm(token)) 46 | 47 | def test_invalid_confirmation_token(self): 48 | u1 = User(password='cat') 49 | u2 = User(password='dog') 50 | db.session.add(u1) 51 | db.session.add(u2) 52 | db.session.commit() 53 | token = u1.generate_confirmation_token() 54 | self.assertFalse(u2.confirm(token)) 55 | 56 | def test_expired_confirmation_token(self): 57 | u = User(password='cat') 58 | db.session.add(u) 59 | db.session.commit() 60 | token = u.generate_confirmation_token(1) 61 | time.sleep(2) 62 | self.assertFalse(u.confirm(token)) 63 | 64 | def test_valid_reset_token(self): 65 | u = User(password='cat') 66 | db.session.add(u) 67 | db.session.commit() 68 | token = u.generate_reset_token() 69 | self.assertTrue(u.reset_password(token, 'dog')) 70 | self.assertTrue(u.verify_password('dog')) 71 | 72 | def test_invalid_reset_token(self): 73 | u1 = User(password='cat') 74 | u2 = User(password='dog') 75 | db.session.add(u1) 76 | db.session.add(u2) 77 | db.session.commit() 78 | token = u1.generate_reset_token() 79 | self.assertFalse(u2.reset_password(token, 'horse')) 80 | self.assertTrue(u2.verify_password('dog')) 81 | 82 | def test_valid_email_change_token(self): 83 | u = User(email='john@example.com', password='cat') 84 | db.session.add(u) 85 | db.session.commit() 86 | token = u.generate_email_change_token('susan@example.org') 87 | self.assertTrue(u.change_email(token)) 88 | self.assertTrue(u.email == 'susan@example.org') 89 | 90 | def test_invalid_email_change_token(self): 91 | u1 = User(email='john@example.com', password='cat') 92 | u2 = User(email='susan@example.org', password='dog') 93 | db.session.add(u1) 94 | db.session.add(u2) 95 | db.session.commit() 96 | token = u1.generate_email_change_token('david@example.net') 97 | self.assertFalse(u2.change_email(token)) 98 | self.assertTrue(u2.email == 'susan@example.org') 99 | 100 | def test_duplicate_email_change_token(self): 101 | u1 = User(email='john@example.com', password='cat') 102 | u2 = User(email='susan@example.org', password='dog') 103 | db.session.add(u1) 104 | db.session.add(u2) 105 | db.session.commit() 106 | token = u2.generate_email_change_token('john@example.com') 107 | self.assertFalse(u2.change_email(token)) 108 | self.assertTrue(u2.email == 'susan@example.org') 109 | 110 | def test_roles_and_permissions(self): 111 | u = User(email='john@example.com', password='cat') 112 | self.assertTrue(u.can(Permission.WRITE_ARTICLES)) 113 | self.assertFalse(u.can(Permission.MODERATE_COMMENTS)) 114 | 115 | def test_anonymous_user(self): 116 | u = AnonymousUser() 117 | self.assertFalse(u.can(Permission.FOLLOW)) 118 | 119 | def test_timestamps(self): 120 | u = User(password='cat') 121 | db.session.add(u) 122 | db.session.commit() 123 | self.assertTrue( 124 | (datetime.utcnow() - u.member_since).total_seconds() < 3) 125 | self.assertTrue( 126 | (datetime.utcnow() - u.last_seen).total_seconds() < 3) 127 | 128 | def test_ping(self): 129 | u = User(password='cat') 130 | db.session.add(u) 131 | db.session.commit() 132 | time.sleep(2) 133 | last_seen_before = u.last_seen 134 | u.ping() 135 | self.assertTrue(u.last_seen > last_seen_before) 136 | 137 | def test_gravatar(self): 138 | u = User(email='john@example.com', password='cat') 139 | with self.app.test_request_context('/'): 140 | gravatar = u.gravatar() 141 | gravatar_256 = u.gravatar(size=256) 142 | gravatar_pg = u.gravatar(rating='pg') 143 | gravatar_retro = u.gravatar(default='retro') 144 | with self.app.test_request_context('/', base_url='https://example.com'): 145 | gravatar_ssl = u.gravatar() 146 | self.assertTrue('http://www.gravatar.com/avatar/' + 147 | 'd4c74594d841139328695756648b6bd6'in gravatar) 148 | self.assertTrue('s=256' in gravatar_256) 149 | self.assertTrue('r=pg' in gravatar_pg) 150 | self.assertTrue('d=retro' in gravatar_retro) 151 | self.assertTrue('https://secure.gravatar.com/avatar/' + 152 | 'd4c74594d841139328695756648b6bd6' in gravatar_ssl) 153 | 154 | def test_follows(self): 155 | u1 = User(email='john@example.com', password='cat') 156 | u2 = User(email='susan@example.org', password='dog') 157 | db.session.add(u1) 158 | db.session.add(u2) 159 | db.session.commit() 160 | self.assertFalse(u1.is_following(u2)) 161 | self.assertFalse(u1.is_followed_by(u2)) 162 | timestamp_before = datetime.utcnow() 163 | u1.follow(u2) 164 | db.session.add(u1) 165 | db.session.commit() 166 | timestamp_after = datetime.utcnow() 167 | self.assertTrue(u1.is_following(u2)) 168 | self.assertFalse(u1.is_followed_by(u2)) 169 | self.assertTrue(u2.is_followed_by(u1)) 170 | self.assertTrue(u1.followed.count() == 2) 171 | self.assertTrue(u2.followers.count() == 2) 172 | f = u1.followed.all()[-1] 173 | self.assertTrue(f.followed == u2) 174 | self.assertTrue(timestamp_before <= f.timestamp <= timestamp_after) 175 | f = u2.followers.all()[-1] 176 | self.assertTrue(f.follower == u1) 177 | u1.unfollow(u2) 178 | db.session.add(u1) 179 | db.session.commit() 180 | self.assertTrue(u1.followed.count() == 1) 181 | self.assertTrue(u2.followers.count() == 1) 182 | self.assertTrue(Follow.query.count() == 2) 183 | u2.follow(u1) 184 | db.session.add(u1) 185 | db.session.add(u2) 186 | db.session.commit() 187 | db.session.delete(u2) 188 | db.session.commit() 189 | self.assertTrue(Follow.query.count() == 1) 190 | -------------------------------------------------------------------------------- /weibo.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, request, jsonify 2 | from flask_oauthlib.client import OAuth 3 | 4 | 5 | app = Flask(__name__) 6 | app.debug = True 7 | app.secret_key = 'development' 8 | oauth = OAuth(app) 9 | 10 | weibo = oauth.remote_app( 11 | 'weibo', 12 | consumer_key='XXX', 13 | consumer_secret='XXX', 14 | request_token_params={'scope': 'email,statuses_to_me_read'}, 15 | base_url='https://api.weibo.com/2/', 16 | authorize_url='https://api.weibo.com/oauth2/authorize', 17 | request_token_url=None, 18 | access_token_method='POST', 19 | access_token_url='https://api.weibo.com/oauth2/access_token', 20 | # since weibo's response is a shit, we need to force parse the content 21 | content_type='application/json', 22 | ) 23 | 24 | 25 | @app.route('/') 26 | def index(): 27 | if 'oauth_token' in session: 28 | access_token = session['oauth_token'][0] 29 | resp = weibo.get('statuses/home_timeline.json') 30 | return jsonify(resp.data) 31 | return redirect(url_for('login')) 32 | 33 | 34 | @app.route('/login') 35 | def login(): 36 | return weibo.authorize(callback=url_for('authorized', 37 | next=request.args.get('next') or request.referrer or None, 38 | _external=True)) 39 | 40 | 41 | @app.route('/logout') 42 | def logout(): 43 | session.pop('oauth_token', None) 44 | return redirect(url_for('index')) 45 | 46 | 47 | @app.route('/login/authorized') 48 | def authorized(): 49 | resp = weibo.authorized_response() 50 | if resp is None: 51 | return 'Access denied: reason=%s error=%s' % ( 52 | request.args['error_reason'], 53 | request.args['error_description'] 54 | ) 55 | session['oauth_token'] = (resp['access_token'], '') 56 | return redirect(url_for('index')) 57 | 58 | 59 | @weibo.tokengetter 60 | def get_weibo_oauth_token(): 61 | return session.get('oauth_token') 62 | 63 | 64 | def change_weibo_header(uri, headers, body): 65 | """Since weibo is a rubbish server, it does not follow the standard, 66 | we need to change the authorization header for it.""" 67 | auth = headers.get('Authorization') 68 | if auth: 69 | auth = auth.replace('Bearer', 'OAuth2') 70 | headers['Authorization'] = auth 71 | return uri, headers, body 72 | 73 | weibo.pre_request = change_weibo_header 74 | 75 | 76 | if __name__ == '__main__': 77 | app.run() 78 | --------------------------------------------------------------------------------