Full Code of xtg20121013/blog_xtg for AI

master 84b95fbb78a1 cached
107 files
1.0 MB
327.9k tokens
363 symbols
1 requests
Download .txt
Showing preview only (1,110K chars total). Download the full file or copy to clipboard to get everything.
Repository: xtg20121013/blog_xtg
Branch: master
Commit: 84b95fbb78a1
Files: 107
Total size: 1.0 MB

Directory structure:
gitextract_mcc003ff/

├── .gitignore
├── README.md
├── alembic/
│   ├── README
│   ├── env.py
│   ├── script.py.mako
│   └── versions/
│       └── 753ec9bc0d27_init_v1_0.py
├── alembic.ini
├── config.py
├── controller/
│   ├── __init__.py
│   ├── admin.py
│   ├── admin_article.py
│   ├── admin_article_type.py
│   ├── admin_custom.py
│   ├── base.py
│   ├── home.py
│   └── super.py
├── docker/
│   ├── Dockerfile
│   ├── entrypoint.sh
│   ├── nginx.conf
│   └── supervisord.conf
├── extends/
│   ├── __init__.py
│   ├── cache_tornadis.py
│   ├── pub_sub_tornadis.py
│   ├── session_redis.py
│   ├── session_tornadis.py
│   ├── time_task.py
│   └── utils.py
├── log_config.py
├── main.py
├── model/
│   ├── __init__.py
│   ├── constants.py
│   ├── logined_user.py
│   ├── models.py
│   ├── pager.py
│   ├── search_params/
│   │   ├── __init__.py
│   │   ├── article_params.py
│   │   ├── article_type_params.py
│   │   ├── comment_params.py
│   │   ├── menu_params.py
│   │   └── plugin_params.py
│   └── site_info.py
├── requirements.txt
├── service/
│   ├── __init__.py
│   ├── article_service.py
│   ├── article_type_service.py
│   ├── blog_view_service.py
│   ├── comment_service.py
│   ├── custom_service.py
│   ├── init_service.py
│   ├── menu_service.py
│   ├── plugin_service.py
│   ├── pubsub_service.py
│   └── user_service.py
├── static/
│   ├── css/
│   │   ├── bootstrap-theme.css
│   │   ├── bootstrap.css
│   │   ├── common.css
│   │   └── prism.css
│   ├── js/
│   │   ├── admin.js
│   │   ├── articleDetail.js
│   │   ├── bootstrap.js
│   │   ├── floatButton.js
│   │   ├── markdown/
│   │   │   ├── bootstrap-markdown.js
│   │   │   ├── locale/
│   │   │   │   └── bootstrap-markdown.zh.js
│   │   │   ├── markdown.js
│   │   │   └── to-markdown.js
│   │   ├── markdownEdit.js
│   │   ├── npm.js
│   │   ├── super.js
│   │   └── tinymce_setup.js
│   └── tinymce/
│       ├── LICENSE.TXT
│       ├── changelog.txt
│       └── js/
│           └── tinymce/
│               ├── extentsion_self/
│               │   └── codesimple_extentsion/
│               │       └── prism.js
│               ├── langs/
│               │   ├── readme.md
│               │   └── zh_CN.js
│               ├── license.txt
│               ├── plugins/
│               │   ├── codesample/
│               │   │   └── css/
│               │   │       └── prism.css
│               │   ├── example/
│               │   │   └── dialog.html
│               │   ├── media/
│               │   │   └── moxieplayer.swf
│               │   └── visualblocks/
│               │       └── css/
│               │           └── visualblocks.css
│               └── skins/
│                   └── myskin/
│                       ├── Variables.less
│                       ├── fonts/
│                       │   ├── readme.md
│                       │   ├── tinymce-small.json
│                       │   └── tinymce.json
│                       └── skin.json
├── template/
│   ├── 403.html
│   ├── 404.html
│   ├── 500.html
│   ├── _article_comments.html
│   ├── _macros.html
│   ├── admin/
│   │   ├── admin_account.html
│   │   ├── admin_base.html
│   │   ├── blog_plugin_add.html
│   │   ├── blog_plugin_edit.html
│   │   ├── custom_blog_info.html
│   │   ├── custom_blog_plugin.html
│   │   ├── help_page.html
│   │   ├── manage_articleTypes.html
│   │   ├── manage_articleTypes_nav.html
│   │   ├── manage_articles.html
│   │   ├── manage_comments.html
│   │   └── submit_articles.html
│   ├── article_detials.html
│   ├── auth/
│   │   └── login.html
│   ├── base.html
│   ├── index.html
│   └── super/
│       └── init.html
└── url_mapping.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
logs


================================================
FILE: README.md
================================================
[blog_xtg](https://github.com/xtg20121013/blog_xtg)是我个人写的一个开源分布式博客,其web框架使用的是tornado(一个基于异步IO的python web框架)。同时我把它设计成一个可以多进程多主机部署的分布式架构,如果你对异步IO的web框架感兴趣,或者对高并发分布式的架构感兴趣并处于入门阶段,那么很希望你来尝试blog_xtg,一定会有所收获。

### 一、为什么写blog_xtg
作为一个码农怎么能没有一个属于自己的个人博客呢?即便没人看,作为日记来记录编码生涯也是很有必要。其实开源的blog有很多,比如WordPress、LifeType等等,但是There are a thousand Hamlets in a thousand people's eyes(一千个读者眼里有一千个哈姆雷特),所以我还是喜欢自己写属于自己的"哈姆雷特"。既然要做新项目,那不用点新东西就会觉得没有意义。恰逢当时淘宝双11,双11会场的页面都是由node.js支撑,node.js做web项目最大的特点就是异步IO,我js不怎么熟,我就选择了python的异步IO框架tornado。但是单个tornado实例无法充分利用多核CPU的资源,所以就实现了blog_xtg这样一个简单的基于tornado的分布式架构博客。

### 二、blog_xtg简介
首先非常感谢开源博客[Blog_mini](https://github.com/xpleaf/Blog_mini),因为整个blog_xtg是基于[Blog_mini](https://github.com/xpleaf/Blog_mini)重构的。

我不太擅长前端,所以基本照搬[Blog_mini](https://github.com/xpleaf/Blog_mini)的页面,但是整个后端逻辑都是重写的,以下是与[Blog_mini](https://github.com/xpleaf/Blog_mini)的主要区别:

1. 改用tornado框架,是个基于异步IO的web server。
2. 分布式架构,可以多进程多主机启动server实例,再通过nginx等代理服务器做负载均衡,实现横向扩展提高并发性能。
3. 提高多数主要页面访问性能。对频繁查询的组件(例如博客标题、菜单、公告、访问统计)进行缓存,优化sql查询(多条sql语句合并一次执行、仅查需要的字段,例如搜索博文列表不查博文的具体内容)以提高首页博文等主要页面访问性能。
4. 访问统计改为日pv和日uv。
5. 博文编辑器改为markdown编辑器。
6. 引入alembic管理数据库版本。
7. 可使用docker快速部署。

但是,作为一个个人blog,其实并不需要分布式的架构,即便引入了这样的架构,我依然希望其他开发者能够快捷的搭建环境并上手使用,因此blog_xtg只是简单的实现了分布式,并不能保证绝对的高可用,主从需要启动实例时手动指定,存在单点故障的可能,如果有开发者希望以此架构扩展到大型生产环境请自行配合zookeeper等实现动态选主+完整的日志分析、性能监控以及完善报警机制来保证高可用。

**注:** blog_xtg目前架构并不需要考虑线程安全问题,因为tornado是单线程的,仅用到多线程的地方只有通过线程池访问数据库,数据库连接session是线程局部变量,其他并无线程间共享的变量,不会带来线程安全问题。

### 三、blog_xtg部署与开发环境搭建
#### 1. 如果你熟悉docker,那么可以用docker来快速部署。
	
	#新建数据库(理论上支持sqlalchemy支持的所有数据库,表会自动创建更新)
	#搭建redis
	#下载config.py并编辑相关配置(修改数据库、redis、日志等)
	curl -o xxx/config.py https://raw.githubusercontent.com/xtg20121013/blog_xtg/master/config.py
	#通过docker启动后即可访问
	docker run -d -p 80:80 --restart=always --name blog_xtg -v xxx/config.py:/home/xtg/blog-xtg/config.py daocloud.io/xtg20121013/blog_xtg:latest
这个镜像启动时包含两个server实例(一主一从)+nginx(动静分离、负载均衡)+supervisor(进程管理),当然你也可以根据自己的需求构建镜像,Dockerfile在项目/docker目录下。
#### 2. 构建运行环境
###### 需要安装以下组件:

1. python2.7(python3 没试过,不知道行不行)
2. mysql(或者其他sqlalchemy支持的数据库)
3. redis

###### clone项目,安装依赖:

	git clone https://github.com/xtg20121013/blog_xtg.git
	#项目依赖(如果用的不是mysql可以将MySQL-python替换使用的数据库成所对应的依赖包)
	pip install -r requirements.txt
###### 创建数据库(注意使用utf-8编码)
###### 启动redis
###### 修改config.py,配置数据库、redis、日志等
###### 创建数据库或更新表
	python main.py upgradedb
###### 启动server
	python main.py --master=true --port=8888

###### 初始化管理员账户
访问http://[host]:[port]/super/init注册管理员账号。

注:仅没有任何管理员时才可以访问到该页面。

### 四、开发注意事项
#### 1.blog_xtg是个异步IO的架构,相对于常见的同步IO框架,需要注意以下几点:

- IO密集型的操作请务必使用异步的client,否则无法利用到异步的优势
- 由于多数异步IO的框架都是单线程的,所以对于CPU密集型的操作最好交由外部系统处理,防止阻塞,大型项目可以配合消息队列使用更佳
- 如果必须用同步的IO组件,可以配合线程池使用(blog_xtg中使用了sqlalchemy就是配合线程池使用的)
- 如果你是ORM+线程池使用(blog_xtg中就是sqlalchemy+线程池),一般的ORM都有lazy load的机制,在异步框架中请勿使用,因为lazy load的执行在主线程中,很可能会阻塞主线程,影响别的请求。

#### 2.blog_xtg是分布式的架构,相对于单进程的项目一般需要注意以下几点:

- 多实例间的日志冲突。
- 多实例间的缓存同步。
- 多实例间的session同步。
- 多实例间主从关系,例如一些定时任务可能主需要集群中一个节点处理。

当然以上几点都可以从blog_xtg的源代码中找到至少一种解决方案。

如果你对异步IO的web框架、分布式的架构感兴趣,或者想对blog_xtg做二次开发,那么你可以阅读以下blog_xtg的其他相关博文,并配合源代码学习,一定会很快掌握。

1. [开源博客blog_xtg技术架构-非阻塞IO web框架tornado](http://blog.52xtg.com/article/10)


#### 3.对于博文编辑的markdown的问题:

我用的是[Bootstrap Markdown](http://www.codingdrama.com/bootstrap-markdown),好像只支持标准的markdown语法,可能大家对代码段的标注语法只知道```的形式,而真正的标准语法是代码段的每一行开头添加4个空格,如果大家不喜欢的话可以尝试更换为[marked](https://github.com/chjj/marked),参见:[修复markdown编辑器无法编写多行code的问题 #2](https://github.com/xtg20121013/blog_xtg/pull/2)

### 五、技术支持
如果你有任何疑问,可以给我留言:

附:	

- 个人博客:[http://blog.52xtg.com](http://blog.52xtg.com)

- 简书博客:[http://www.jianshu.com/u/dfb6bf87c35e](http://www.jianshu.com/u/dfb6bf87c35e)

- 试用博客:[http://blogdemo.52xtg.com](http://blogdemo.52xtg.com)

- blog_xtg的github地址:[https://github.com/xtg20121013/blog_xtg](https://github.com/xtg20121013/blog_xtg)


================================================
FILE: alembic/README
================================================
Generic single-database configuration.

================================================
FILE: alembic/env.py
================================================
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from config import database_config
from model.models import DbBase

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = DbBase.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline():
    """Run migrations in 'offline' mode.

    This configures the context with just a URL
    and not an Engine, though an Engine is acceptable
    here as well.  By skipping the Engine creation
    we don't even need a DBAPI to be available.

    Calls to context.execute() here emit the given string to the
    script output.

    """
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url, target_metadata=target_metadata, literal_binds=True)

    with context.begin_transaction():
        context.run_migrations()


def run_migrations_online():
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    connectable = engine_from_config(
        {'sqlalchemy.url': database_config['engine_url']},
        prefix='sqlalchemy.',
        poolclass=pool.NullPool)

    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()

if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()


================================================
FILE: alembic/script.py.mako
================================================
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}


def upgrade():
    ${upgrades if upgrades else "pass"}


def downgrade():
    ${downgrades if downgrades else "pass"}


================================================
FILE: alembic/versions/753ec9bc0d27_init_v1_0.py
================================================
# coding=utf-8
"""init_v1_0

Revision ID: 753ec9bc0d27
Revises: 
Create Date: 2017-03-12 20:17:20.958379

"""
from alembic import op
import sqlalchemy as sa
from model.constants import Constants

# revision identifiers, used by Alembic.
revision = '753ec9bc0d27'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    ats = op.create_table('articleTypeSettings',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=64), nullable=True),
    sa.Column('protected', sa.Boolean(), nullable=True),
    sa.Column('hide', sa.Boolean(), nullable=True),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('name')
    )
    blog_info = op.create_table('blog_info',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('title', sa.String(length=64), nullable=True),
    sa.Column('signature', sa.Text(), nullable=True),
    sa.Column('navbar', sa.String(length=64), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_table('blog_view',
    sa.Column('date', sa.DATE(), nullable=False),
    sa.Column('pv', sa.BigInteger(), nullable=True),
    sa.Column('uv', sa.BigInteger(), nullable=True),
    sa.PrimaryKeyConstraint('date')
    )
    op.create_table('menus',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=64), nullable=True),
    sa.Column('order', sa.Integer(), nullable=False),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('name')
    )
    plugins = op.create_table('plugins',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('title', sa.String(length=64), nullable=True),
    sa.Column('note', sa.Text(), nullable=True),
    sa.Column('content', sa.Text(), nullable=True),
    sa.Column('order', sa.Integer(), nullable=True),
    sa.Column('disabled', sa.Boolean(), nullable=True),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('title')
    )
    op.create_index(op.f('ix_plugins_order'), 'plugins', ['order'], unique=False)
    sources = op.create_table('sources',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=64), nullable=True),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('name')
    )
    op.create_table('users',
    sa.Column('created_at', sa.DateTime(), nullable=True),
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('email', sa.String(length=64), nullable=True),
    sa.Column('username', sa.String(length=64), nullable=True),
    sa.Column('password', sa.String(length=128), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
    op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
    articleTypes = op.create_table('articleTypes',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=64), nullable=True),
    sa.Column('introduction', sa.Text(), nullable=True),
    sa.Column('menu_id', sa.Integer(), nullable=True),
    sa.Column('setting_id', sa.Integer(), nullable=True),
    sa.ForeignKeyConstraint(['menu_id'], ['menus.id'], ),
    sa.ForeignKeyConstraint(['setting_id'], ['articleTypeSettings.id'], ),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('name')
    )
    op.create_table('articles',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('title', sa.String(length=64), nullable=True),
    sa.Column('content', sa.Text(), nullable=True),
    sa.Column('summary', sa.Text(), nullable=True),
    sa.Column('create_time', sa.DateTime(), nullable=True),
    sa.Column('update_time', sa.DateTime(), nullable=True),
    sa.Column('num_of_view', sa.Integer(), nullable=True),
    sa.Column('articleType_id', sa.Integer(), nullable=True),
    sa.Column('source_id', sa.Integer(), nullable=True),
    sa.ForeignKeyConstraint(['articleType_id'], ['articleTypes.id'], ),
    sa.ForeignKeyConstraint(['source_id'], ['sources.id'], ),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_articles_create_time'), 'articles', ['create_time'], unique=False)
    op.create_index(op.f('ix_articles_update_time'), 'articles', ['update_time'], unique=False)
    op.create_table('comments',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('content', sa.Text(), nullable=True),
    sa.Column('create_time', sa.DateTime(), nullable=True),
    sa.Column('author_name', sa.String(length=64), nullable=True),
    sa.Column('author_email', sa.String(length=64), nullable=True),
    sa.Column('article_id', sa.Integer(), nullable=True),
    sa.Column('disabled', sa.Boolean(), nullable=True),
    sa.Column('comment_type', sa.String(length=64), nullable=True),
    sa.Column('rk', sa.String(length=64), nullable=True),
    sa.Column('floor', sa.Integer(), nullable=False),
    sa.Column('reply_to_id', sa.Integer(), nullable=True),
    sa.Column('reply_to_floor', sa.String(length=64), nullable=True),
    sa.ForeignKeyConstraint(['article_id'], ['articles.id'], ),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###
    # insert default data
    op.bulk_insert(ats, [
        dict(id=1, name='system', protected=True, hide=True)
    ])
    op.bulk_insert(blog_info,[
        dict(id=1,
             title=u'开源分布式博客系统blog_xtg',
             signature=u'基于tornado的分布式博客!— by xtg',
             navbar='inverse')
    ])
    op.bulk_insert(plugins, [
        dict(id=1,
             title=u'博客统计',
             note=u'系统插件',
             content='system_plugin',
             order=1,
             disabled=False)
    ])
    op.bulk_insert(sources, [
        dict(id=1, name=u'原创', ),
        dict(id=2, name=u'转载', ),
        dict(id=3, name=u'翻译', ),
    ])
    op.bulk_insert(articleTypes, [
        dict(id=Constants.ARTICLE_TYPE_DEFAULT_ID,
             name=u'未分类',
             introduction=u'系统默认分类,不可删除。',
             setting_id=1,
        ),
    ])


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('comments')
    op.drop_index(op.f('ix_articles_update_time'), table_name='articles')
    op.drop_index(op.f('ix_articles_create_time'), table_name='articles')
    op.drop_table('articles')
    op.drop_table('articleTypes')
    op.drop_index(op.f('ix_users_username'), table_name='users')
    op.drop_index(op.f('ix_users_email'), table_name='users')
    op.drop_table('users')
    op.drop_table('sources')
    op.drop_index(op.f('ix_plugins_order'), table_name='plugins')
    op.drop_table('plugins')
    op.drop_table('menus')
    op.drop_table('blog_view')
    op.drop_table('blog_info')
    op.drop_table('articleTypeSettings')
    # ### end Alembic commands ###


================================================
FILE: alembic.ini
================================================
# A generic, single database configuration.

[alembic]
# path to migration scripts
script_location = alembic

# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; this defaults
# to alembic/versions.  When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

sqlalchemy.url = driver://user:pass@localhost/dbname


# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S


================================================
FILE: config.py
================================================
# coding=utf-8
from urllib import quote_plus as urlquote

cookie_keys = dict(
    session_key_name="TR_SESSION_ID",
    uv_key_name="uv_tag",
)

# session相关配置(redis实现)
redis_session_config = dict(
    db_no=0,
    host="127.0.0.1",
    port=6379,
    password=None,
    max_connections=10,
    session_key_name=cookie_keys['session_key_name'],
    session_expires_days=7,
)

# 站点缓存(redis)
site_cache_config = dict(
    db_no=1,
    host="127.0.0.1",
    port=6379,
    password=None,
    max_connections=10,
)

# 基于redis的消息订阅(发布接收缓存更新消息)
redis_pub_sub_channels = dict(
    cache_message_channel="site_cache_message_channel",
)

# 消息订阅(基于redis)配置
redis_pub_sub_config = dict(
    host="127.0.0.1",
    port=6379,
    password=None,
    autoconnect=True,
    channels=[redis_pub_sub_channels['cache_message_channel'],],
)

# 数据库配置
database_config = dict(
    engine=None,
    # engine_url='postgresql+psycopg2://mhq:1qaz2wsx@localhost:5432/blog',
    # 如果是使用mysql+mysqldb,在确认所有的库表列都是uft8编码后,依然有字符编码报错,
    # 可以尝试在该url末尾加上queryString charset=utf8
    engine_url='mysql+mysqlconnector://root:%s@localhost:3306/blog_xtg?charset=utf8' % urlquote('MyPass@123'),
    engine_setting=dict(
        echo=False,  # print sql
        echo_pool=False,
        # 设置7*60*60秒后回收连接池,默认-1,从不重置
        # 该参数会在每个session调用执行sql前校验当前时间与上一次连接时间间隔是否超过pool_recycle,如果超过就会重置。
        # 这里设置7小时是为了避免mysql默认会断开超过8小时未活跃过的连接,避免"MySQL server has gone away”错误
        # 如果mysql重启或断开过连接,那么依然会在第一次时报"MySQL server has gone away",
        # 假如需要非常严格的mysql断线重连策略,可以设置心跳。
        # 心跳设置参考https://stackoverflow.com/questions/18054224/python-sqlalchemy-mysql-server-has-gone-away
        pool_recycle=25200,
        pool_size=20,
        max_overflow=20,
    ),
)

session_keys = dict(
    login_user="login_user",
    messages="messages",
    article_draft="article_draft",
)

# 关联model.site_info中的字段
site_cache_keys = dict(
    title="title",
    signature="signature",
    navbar="navbar",
    menus="menus",
    article_types_not_under_menu="article_types_not_under_menu",
    plugins="plugins",
    pv="pv",
    uv="uv",
    article_count="article_count",
    comment_count="comment_count",
    article_sources="article_sources",
    source_articles_count="source_{}_articles_count",
)

# 站点相关配置以及tornado的相关参数
config = dict(
    debug=False,
    log_level="INFO",
    log_console=True,
    log_file=False,
    log_file_path="logs/log",  # 末尾自动添加 @端口号.txt_日期
    compress_response=True,
    xsrf_cookies=True,
    cookie_secret="kjsdhfweiofjhewnfiwehfneiwuhniu",
    login_url="/auth/login",
    port=8888,
    max_threads_num=500,
    database=database_config,
    redis_session=redis_session_config,
    session_keys=session_keys,
    master=True,  # 是否为主从节点中的master节点, 整个集群有且仅有一个,(要提高可用性的话可以用zookeeper来选主,该项目就暂时不做了)
    navbar_styles={"inverse": "魅力黑", "default": "优雅白"},  # 导航栏样式
    default_avatar_url="identicon",
    application=None,  # 项目启动后会在这里注册整个server,以便在需要的地方调用,勿修改
)

================================================
FILE: controller/__init__.py
================================================
# coding=utf-8


================================================
FILE: controller/admin.py
================================================
# coding=utf-8
from base import BaseHandler
from tornado.gen import coroutine
from tornado.web import authenticated
from service.user_service import UserService


class AdminAccountHandler(BaseHandler):

    @authenticated
    def get(self):
        self.render("admin/admin_account.html")

    @coroutine
    def post(self, require):
        if require == "edit-user-info":
            yield self.edit_user_info()
        elif require == "change-password":
            yield self.change_password()

    @authenticated
    @coroutine
    def edit_user_info(self):
        user_info = {"username": self.get_argument("username"), "email": self.get_argument("email")}
        user = yield self.async_do(UserService.update_user_info, self.db, self.current_user.name,
                                   self.get_argument("password"), user_info)
        if user:
            self.save_login_user(user)
            self.add_message('success', u'修改用户信息成功!')
        else:
            self.add_message('danger', u'修改用户信息失败!密码不正确!')
        self.redirect(self.reverse_url("admin.account"))

    @authenticated
    @coroutine
    def change_password(self):
        old_password = self.get_argument("old_password")
        new_password = self.get_argument("password")
        count = yield self.async_do(UserService.update_password, self.db, self.current_user.name,
                                    old_password, new_password)
        if count > 0:
            self.add_message('success', u'修改密码成功!')
        else:
            self.add_message('danger', u'修改密码失败!')
        self.redirect(self.reverse_url("admin.account"))


class AdminHelpHandler(BaseHandler):

    @authenticated
    def get(self):
        self.render('admin/help_page.html')

================================================
FILE: controller/admin_article.py
================================================
# coding=utf-8
from tornado.gen import coroutine
from tornado.web import authenticated

from base import BaseHandler
from config import session_keys
from model.models import Article
from model.constants import Constants
from service.article_service import ArticleService
from service.article_type_service import ArticleTypeService
from service.init_service import SiteCacheService
from service.comment_service import CommentService
from model.search_params.article_params import ArticleSearchParams
from model.search_params.comment_params import CommentSearchParams
from model.pager import Pager


class ArticleAndCommentsFlush(object):
    @coroutine
    def flush_article_cache(self, action, article):
        yield SiteCacheService.update_article_action(self.cache_manager, action, article,
                                                     is_pub_all=True, pubsub_manager=self.pubsub_manager)

    @coroutine
    def flush_comments_cache(self, action, comments):
        yield SiteCacheService.update_comment_action(self.cache_manager, action, comments,
                                                     is_pub_all=True, pubsub_manager=self.pubsub_manager)


class AdminArticleHandler(BaseHandler, ArticleAndCommentsFlush):

    @coroutine
    def get(self, *require):
        if require:
            if len(require) == 1:
                action = require[0]
                if action == 'submit':
                    yield self.submit_get()
                elif action.isdigit():
                    article_id = int(action)
                    yield self.article_get(article_id)
        else:
            yield self.page_get()

    @coroutine
    def post(self, *require):
        if require:
            if len(require) == 1:
                if require[0] == 'submit':
                    yield self.submit_post()
                elif require[0].isdigit():
                    article_id = int(require[0])
                    yield self.update_post(article_id)
            elif len(require) == 2:
                article_id = require[0]
                action = require[1]
                if action == 'delete':
                    yield self.delete_post(article_id)

    @coroutine
    @authenticated
    def page_get(self):
        pager = Pager(self)
        article_search_params = ArticleSearchParams(self)
        article_types = yield self.async_do(ArticleTypeService.list_simple, self.db)
        pager = yield self.async_do(ArticleService.page_articles, self.db, pager, article_search_params)
        self.render("admin/manage_articles.html", article_types=article_types, pager=pager,
                    article_search_params=article_search_params)

    @coroutine
    @authenticated
    def article_get(self, article_id):
        article_types = yield self.async_do(ArticleTypeService.list_simple, self.db)
        article = yield self.async_do(ArticleService.get_article_all, self.db, article_id, True)
        self.render("admin/submit_articles.html", article_types=article_types, article=article)

    @coroutine
    @authenticated
    def submit_get(self):
        article_draft = self.session.pop(session_keys['article_draft'], None)
        article = None
        if article_draft:
            source_id = article_draft.get("source_id")
            type_id = article_draft.get("articleType_id")
            article = Article(source_id=int(source_id) if source_id else None,
                              title=article_draft.get("title"),
                              articleType_id=int(type_id) if type_id else None,
                              content=article_draft.get("content"),
                              summary=article_draft.get("summary"))
        article_types = yield self.async_do(ArticleTypeService.list_simple, self.db)
        self.render("admin/submit_articles.html", article_types=article_types, article=article)

    @coroutine
    @authenticated
    def submit_post(self):
        article = dict(
            source_id=self.get_argument("source_id"),
            title=self.get_argument("title"),
            articleType_id=self.get_argument("articleType_id"),
            content=self.get_argument("content"),
            summary=self.get_argument("summary"),
        )
        article_saved = yield self.async_do(ArticleService.add_article, self.db, article)
        if article_saved and article_saved.id:
            yield self.flush_article_cache(Constants.FLUSH_ARTICLE_ACTION_ADD, article_saved)
            self.add_message('success', u'保存成功!')
            self.redirect(self.reverse_url('article', article_saved.id))
        else:
            self.add_message('danger', u'保存失败!')
            self.session[session_keys['article_draft']] = article
            self.redirect(self.reverse_url('admin.article.action', 'submit'))

    @coroutine
    @authenticated
    def update_post(self, article_id):
        article = dict(
            id=article_id,
            source_id=self.get_argument("source_id"),
            title=self.get_argument("title"),
            articleType_id=self.get_argument("articleType_id"),
            content=self.get_argument("content"),
            summary=self.get_argument("summary"),
        )
        article_updateds = yield self.async_do(ArticleService.update_article, self.db, article)
        if article_updateds:
            yield self.flush_article_cache(Constants.FLUSH_ARTICLE_ACTION_UPDATE, article=article_updateds)
            article_updated = article_updateds[0]
            self.add_message('success', u'修改成功!')
            self.redirect(self.reverse_url('article', article_updated.id))
        else:
            self.add_message('danger', u'修改失败!')
            self.redirect(self.reverse_url('admin.article', article_id))

    @coroutine
    @authenticated
    def delete_post(self, article_id):
        article_deleted, comments_deleted = yield self.async_do(ArticleService.delete_article, self.db, article_id)
        if article_deleted:
            yield self.flush_article_cache(Constants.FLUSH_ARTICLE_ACTION_REMOVE, article_deleted)
            yield self.flush_comments_cache(Constants.FLUSH_COMMENT_ACTION_REMOVE, comments_deleted)
            self.add_message('success', u'删除成功,并删除{}条评论!'.format(len(comments_deleted)))
            self.write("success")
        else:
            self.add_message('danger', u'删除失败!')
            self.write("error")


class AdminArticleCommentHandler(BaseHandler, ArticleAndCommentsFlush):
    @coroutine
    def get(self, *require):
        yield self.page_get()

    @coroutine
    def post(self, *require):
        if require:
            if len(require) == 3:
                article_id = require[0]
                comment_id = require[1]
                action = require[2]
                if action == 'disable':
                    yield self.disable_post(article_id, comment_id, True)
                elif action == 'enable':
                    yield self.disable_post(article_id, comment_id, False)
                elif action == 'delete':
                    yield self.delete_post(article_id, comment_id)

    @coroutine
    @authenticated
    def page_get(self):
        pager = Pager(self)
        comment_search_params = CommentSearchParams(self)
        comment_search_params.show_article_id_title = True
        comment_search_params.order_mode = CommentSearchParams.ORDER_MODE_CREATE_TIME_DESC
        comments_pager = yield self.async_do(CommentService.page_comments, self.db, pager, comment_search_params)
        self.render("admin/manage_comments.html", pager=comments_pager)

    @coroutine
    @authenticated
    def disable_post(self, article_id, comment_id, disabled):
        updated = yield self.async_do(CommentService.update_comment_disabled, self.db, article_id, comment_id, disabled)
        if updated:
            self.add_message('success', u'修改成功')
            self.write("success")
        else:
            self.add_message('danger', u'修改失败!')
            self.write("error")

    @coroutine
    @authenticated
    def delete_post(self, article_id, comment_id):
        comment_deleted = yield self.async_do(CommentService.delete_comment, self.db, article_id, comment_id)
        if comment_deleted:
            yield self.flush_comments_cache(Constants.FLUSH_COMMENT_ACTION_REMOVE, comment_deleted)
            self.add_message('success', u'删除成功')
            self.write("success")
        else:
            self.add_message('danger', u'删除失败!')
            self.write("error")

================================================
FILE: controller/admin_article_type.py
================================================
# coding=utf-8
from tornado.web import authenticated
from tornado.gen import coroutine
from base import BaseHandler
from model.pager import Pager
from model.search_params.menu_params import MenuSearchParams
from model.search_params.article_type_params import ArticleTypeSearchParams
from service.menu_service import MenuService
from service.init_service import SiteCacheService
from service.article_type_service import ArticleTypeService


class AdminArticleTypeBaseHandler(BaseHandler):
    @coroutine
    def flush_menus(self, menus=None, article_types_not_under_menu=None):
        if menus is None:
            menus = yield self.async_do(MenuService.list_menus, self.db, show_types=True)
        if article_types_not_under_menu is None:
            article_types_not_under_menu = yield \
                self.async_do(ArticleTypeService.list_article_types_not_under_menu, self.db)
        yield SiteCacheService.update_menus(self.cache_manager, menus, article_types_not_under_menu,
                                            is_pub_all=True, pubsub_manager=self.pubsub_manager)


class AdminArticleTypeHandler(AdminArticleTypeBaseHandler):

    @coroutine
    def get(self, *require):
        if require:
            if len(require) == 2:
                article_type_id = require[0]
                action = require[1]
                if action == 'delete':
                    yield self.delete_get(article_type_id)
        else:
            yield self.page_get()

    @coroutine
    def post(self, *require):
        if require:
            if len(require) == 1:
                if require[0] == 'add':
                    yield self.add_post()
            elif len(require) == 2:
                article_type_id = require[0]
                action = require[1]
                if action == 'update':
                    yield self.update_post(article_type_id)

    @coroutine
    @authenticated
    def page_get(self):
        pager = Pager(self)
        search_param = ArticleTypeSearchParams(self)
        search_param.show_setting = True
        search_param.show_articles_count = True
        pager = yield self.async_do(ArticleTypeService.page_article_types, self.db, pager, search_param)
        menus = yield self.async_do(MenuService.list_menus, self.db)
        self.render("admin/manage_articleTypes.html", pager=pager, menus=menus)

    @coroutine
    @authenticated
    def delete_get(self, article_type_id):
        update_count = yield self.async_do(ArticleTypeService.delete, self.db, article_type_id)
        if update_count:
            yield self.flush_menus()
            self.add_message('success', u'删除成功!')
        else:
            self.add_message('danger', u'删除失败!')
        redirect_url = self.reverse_url('admin.articleTypes')
        if self.request.query:
            redirect_url += "?" + self.request.query
        self.redirect(redirect_url)

    @coroutine
    @authenticated
    def add_post(self):
        menu_id = int(self.get_argument("menu_id")) \
            if self.get_argument("menu_id") and self.get_argument("menu_id").isdigit() else None
        article_type = dict(
            name=self.get_argument("name"),
            setting_hide=self.get_argument("setting_hide") == 'true',
            introduction=self.get_argument("introduction"),
            menu_id=menu_id if menu_id > 0 else None,
        )
        added = yield self.async_do(ArticleTypeService.add_article_type, self.db, article_type)
        if added:
            yield self.flush_menus()
            self.add_message('success', u'保存成功!')
        else:
            self.add_message('danger', u'保存失败!')
        redirect_url = self.reverse_url('admin.articleTypes')
        if self.request.query:
            redirect_url += "?" + self.request.query
        self.redirect(redirect_url)

    @coroutine
    @authenticated
    def update_post(self, article_type_id):
        menu_id = int(self.get_argument("menu_id")) \
            if self.get_argument("menu_id") and self.get_argument("menu_id").isdigit() else None
        article_type = dict(
            id=article_type_id,
            name=self.get_argument("name"),
            setting_hide=self.get_argument("setting_hide") == 'true',
            introduction=self.get_argument("introduction"),
            menu_id=menu_id if menu_id > 0 else None,
        )
        updated = yield self.async_do(ArticleTypeService.update_article_type, self.db, article_type_id, article_type)
        if updated:
            yield self.flush_menus()
            self.add_message('success', u'修改成功!')
        else:
            self.add_message('danger', u'修改失败!')
        redirect_url = self.reverse_url('admin.articleTypes')
        if self.request.query:
            redirect_url += "?" + self.request.query
        self.redirect(redirect_url)


class AdminArticleTypeNavHandler(AdminArticleTypeBaseHandler):

    @coroutine
    def get(self, *require):
        if require:
            if len(require) == 2:
                menu_id = require[0]
                action = require[1]
                if action == 'sort-up':
                    yield self.sort_up_get(menu_id)
                elif action == 'sort-down':
                    yield self.sort_down_get(menu_id)
                elif action == 'delete':
                    yield self.delete_get(menu_id)
        else:
            yield self.page_get()

    @coroutine
    def post(self, *require):
        if require:
            if len(require) == 1:
                if require[0] == 'add':
                    yield self.add_post()
            elif len(require) == 2:
                menu_id = require[0]
                action = require[1]
                if action == 'update':
                    yield self.update_post(menu_id)

    @coroutine
    @authenticated
    def add_post(self):
        menu = dict(name=self.get_argument('name'),)
        added = yield self.async_do(MenuService.add_menu, self.db, menu)
        if added:
            yield self.flush_menus()
            self.add_message('success', u'保存成功!')
        else:
            self.add_message('danger', u'保存失败!')
        redirect_url = self.reverse_url('admin.articleTypeNavs')
        if self.request.query:
            redirect_url += "?"+self.request.query
        self.redirect(redirect_url)

    @coroutine
    @authenticated
    def update_post(self, menu_id):
        menu = dict(name=self.get_argument('name'),)
        update_count = yield self.async_do(MenuService.update, self.db, menu_id, menu)
        if update_count:
            yield self.flush_menus()
            self.add_message('success', u'修改成功!')
        else:
            self.add_message('danger', u'保存失败!')
        redirect_url = self.reverse_url('admin.articleTypeNavs')
        if self.request.query:
            redirect_url += "?"+self.request.query
        self.redirect(redirect_url)

    @coroutine
    @authenticated
    def page_get(self):
        pager = Pager(self)
        menu_search_params = MenuSearchParams(self)
        pager = yield self.async_do(MenuService.page_menus, self.db, pager, menu_search_params)
        self.render("admin/manage_articleTypes_nav.html", pager=pager)

    @coroutine
    @authenticated
    def sort_up_get(self, menu_id):
        updated = yield self.async_do(MenuService.sort_up, self.db, menu_id)
        if updated:
            yield self.flush_menus()
            self.add_message('success', u'导航升序成功!')
        else:
            self.add_message('danger', u'操作失败!')
        redirect_url = self.reverse_url('admin.articleTypeNavs')
        if self.request.query:
            redirect_url += "?"+self.request.query
        self.redirect(redirect_url)

    @coroutine
    @authenticated
    def sort_down_get(self, menu_id):
        updated = yield self.async_do(MenuService.sort_down, self.db, menu_id)
        if updated:
            yield self.flush_menus()
            self.add_message('success', u'导航降序成功!')
        else:
            self.add_message('danger', u'操作失败!')
        redirect_url = self.reverse_url('admin.articleTypeNavs')
        if self.request.query:
            redirect_url += "?"+self.request.query
        self.redirect(redirect_url)

    @coroutine
    @authenticated
    def sort_up_get(self, menu_id):
        updated = yield self.async_do(MenuService.sort_up, self.db, menu_id)
        if updated:
            yield self.flush_menus()
            self.add_message('success', u'导航升序成功!')
        else:
            self.add_message('danger', u'操作失败!')
        redirect_url = self.reverse_url('admin.articleTypeNavs')
        if self.request.query:
            redirect_url += "?"+self.request.query
        self.redirect(redirect_url)

    @coroutine
    @authenticated
    def delete_get(self, menu_id):
        update_count = yield self.async_do(MenuService.delete, self.db, menu_id)
        if update_count:
            yield self.flush_menus()
            self.add_message('success', u'删除成功!')
        else:
            self.add_message('danger', u'保存失败!')
        redirect_url = self.reverse_url('admin.articleTypeNavs')
        if self.request.query:
            redirect_url += "?"+self.request.query
        self.redirect(redirect_url)


================================================
FILE: controller/admin_custom.py
================================================
# coding=utf-8
from base import BaseHandler
from tornado.gen import coroutine
from tornado.web import authenticated
from config import config
from model.pager import Pager
from model.search_params.plugin_params import PluginSearchParams
from service.custom_service import BlogInfoService
from service.init_service import SiteCacheService
from service.plugin_service import PluginService


class AdminCustomBlogInfoHandler(BaseHandler):

    @authenticated
    def get(self):
        self.render("admin/custom_blog_info.html", navbar_styles=config['navbar_styles'])

    @coroutine
    @authenticated
    def post(self):
        info = dict(title=self.get_argument("title"), signature=self.get_argument("signature"),
                    navbar=self.get_argument("navbar"),)
        blog_info = yield self.async_do(BlogInfoService.update_blog_info, self.db, info)
        if blog_info:
            #  更新本地及redis缓存,并发布消息通知其他节点更新
            yield self.flush_blog_info(blog_info)
            self.add_message('success', u'修改博客信息成功!')
        else:
            self.add_message('danger', u'修改失败!')
        self.redirect(self.reverse_url("admin.custom.blog_info"))

    @coroutine
    def flush_blog_info(self, blog_info):
        #  更新本地及redis缓存,并发布消息通知其他节点更新
        yield SiteCacheService.update_blog_info(self.cache_manager, blog_info,
                                                is_pub_all=True, pubsub_manager=self.pubsub_manager)


class AdminCustomBlogPluginHandler(BaseHandler):

    @coroutine
    def get(self, *require):
        if require:
            if len(require) == 1:
                if require[0] == 'add':
                    self.add_get()
            elif len(require) == 2:
                plugin_id = require[0]
                action = require[1]
                if action == 'sort-up':
                    yield self.sort_up_get(plugin_id)
                elif action == 'sort-down':
                    yield self.sort_down_get(plugin_id)
                elif action == 'disable':
                    yield self.set_disabled_get(plugin_id, True)
                elif action == 'enable':
                    yield self.set_disabled_get(plugin_id, False)
                elif action == 'delete':
                    yield self.delete_get(plugin_id)
                elif action == 'edit':
                    yield self.edit_get(plugin_id)
        else:
            yield self.index_get()

    @coroutine
    def post(self, *require):
        if require:
            if len(require) == 1:
                if require[0] == 'add':
                    yield self.add_post()
            elif len(require) == 2:
                plugin_id = require[0]
                action = require[1]
                if action == 'edit':
                    yield self.edit_post(plugin_id)

    @coroutine
    @authenticated
    def index_get(self):
        pager = Pager(self)
        plugin_search_params = PluginSearchParams(self)
        pager = yield self.async_do(PluginService.page_plugins, self.db, pager, plugin_search_params)
        self.render("admin/custom_blog_plugin.html", pager=pager)

    @authenticated
    def add_get(self):
        self.render("admin/blog_plugin_add.html")

    @coroutine
    @authenticated
    def edit_get(self, plugin_id):
        plugin = yield self.async_do(PluginService.get, self.db, plugin_id)
        self.render("admin/blog_plugin_edit.html", plugin=plugin)

    @coroutine
    @authenticated
    def sort_up_get(self, plugin_id):
        updated = yield self.async_do(PluginService.sort_up, self.db, plugin_id)
        if updated:
            yield self.flush_plugins()
            self.add_message('success', u'插件升序成功!')
        else:
            self.add_message('danger', u'操作失败!')
        self.redirect(self.reverse_url('admin.custom.blog_plugin')+"?"+self.request.query)

    @coroutine
    @authenticated
    def sort_down_get(self, plugin_id):
        updated = yield self.async_do(PluginService.sort_down, self.db, plugin_id)
        if updated:
            yield self.flush_plugins()
            self.add_message('success', u'插件降序成功!')
        else:
            self.add_message('danger', u'操作失败!')
        self.redirect(self.reverse_url('admin.custom.blog_plugin')+"?"+self.request.query)

    @coroutine
    @authenticated
    def set_disabled_get(self, plugin_id, disabled):
        updated_count = yield self.async_do(PluginService.update_disabled, self.db, plugin_id, disabled)
        if updated_count:
            yield self.flush_plugins()
            self.add_message('success', u'插件禁用成功!')
        else:
            self.add_message('danger', u'操作失败!')
        self.redirect(self.reverse_url('admin.custom.blog_plugin')+"?"+self.request.query)

    @coroutine
    @authenticated
    def delete_get(self, plugin_id):
        updated = yield self.async_do(PluginService.delete, self.db, plugin_id)
        if updated:
            yield self.flush_plugins()
            self.add_message('success', u'插件删除成功!')
        else:
            self.add_message('danger', u'操作失败!')
        self.redirect(self.reverse_url('admin.custom.blog_plugin')+"?"+self.request.query)

    @coroutine
    @authenticated
    def add_post(self):
        plugin = dict(title=self.get_argument('title'),note=self.get_argument('note'),
                      content=self.get_argument('content'),)
        plugin_saved = yield self.async_do(PluginService.save, self.db, plugin)
        if plugin_saved and plugin_saved.id:
            yield self.flush_plugins()
            self.add_message('success', u'保存成功!')
        else:
            self.add_message('danger', u'保存失败!')
        self.redirect(self.reverse_url('admin.custom.plugin.action', 'add'))

    @coroutine
    @authenticated
    def edit_post(self, plugin_id):
        plugin = dict(
            id=plugin_id,
            title=self.get_argument("title", None),
            note=self.get_argument("note", None),
            content=self.get_argument("content", None),
        )
        updated = yield self.async_do(PluginService.update, self.db, plugin_id, plugin)
        if updated:
            yield self.flush_plugins()
            self.add_message('success', u'插件修改成功!')
        else:
            self.add_message('danger', u'操作失败!')
        self.redirect(self.reverse_url('admin.custom.blog_plugin')+"?"+self.request.query)


    @coroutine
    def flush_plugins(self, plugins=None):
        if plugins is None:
            plugins = yield self.async_do(PluginService.list_plugins, self.db)
        yield SiteCacheService.update_plugins(self.cache_manager, plugins,
                                              is_pub_all=True, pubsub_manager=self.pubsub_manager)

================================================
FILE: controller/base.py
================================================
# coding=utf-8
import hashlib
import urllib

import datetime
import tornado.web
from tornado import gen
from tornado.escape import url_escape

from config import session_keys, config, cookie_keys
from extends.session_tornadis import Session
from model.logined_user import LoginUser
from service.init_service import SiteCacheService
from service.blog_view_service import BlogViewService


class BaseHandler(tornado.web.RequestHandler):

    def initialize(self):
        self.session = None
        self.db_session = None
        self.session_save_tag = False
        self.session_expire_time = 604800  # 7*24*60*60秒
        self.thread_executor = self.application.thread_executor
        self.cache_manager = self.application.cache_manager
        self.async_do = self.thread_executor.submit

    def login_url(self):
        return self.get_login_url()+"?next="+url_escape(self.request.uri)

    @gen.coroutine
    def prepare(self):
        yield self.init_session()
        if session_keys['login_user'] in self.session:
            self.current_user = LoginUser(self.session[session_keys['login_user']])
        self.add_pv_uv() # 与主代码异步执行,所以不用yield阻塞

    #  增加pv,uv, 调用该方法可以不用yield阻塞以达到与主代码异步执行
    #  每次调用pv+1, uv根据cookie每24小时只+1
    #  因为要与主代码异步执行,所以要使用独立的db连接
    @gen.coroutine
    def add_pv_uv(self):
        add_pv = 1
        add_uv = 0
        date = datetime.date.today()
        last_view_day = self.get_secure_cookie(cookie_keys['uv_key_name'], None)
        if not last_view_day or int(last_view_day) != date.day:
            add_uv = 1
            self.set_secure_cookie(cookie_keys['uv_key_name'], str(date.day), 1)
        yield SiteCacheService.add_pv_uv(self.cache_manager, add_pv, add_uv,
                                         is_pub_all=True, pubsub_manager=self.pubsub_manager)
        yield self.async_do(BlogViewService.add_blog_view, self.application.db_pool(), add_pv, add_uv, date)

    @gen.coroutine
    def init_session(self):
        if not self.session:
            self.session = Session(self)
            yield self.session.init_fetch()

    def save_session(self):
        self.session_save_tag = True
        self.session.generate_session_id()

    @property
    def db(self):
        if not self.db_session:
            self.db_session = self.application.db_pool()
        return self.db_session

    @property
    def pubsub_manager(self):
        return self.application.pubsub_manager

    def save_login_user(self, user):
        login_user = LoginUser(None)
        login_user['id'] = user.id
        login_user['name'] = user.username
        login_user['avatar'] = self.get_gravatar_url(user.email)
        login_user['email'] = user.email
        self.session[session_keys['login_user']] = login_user
        self.current_user = login_user
        self.save_session()

    def logout(self):
        if session_keys['login_user'] in self.session:
            del self.session[session_keys['login_user']]
            self.save_session()
        self.current_user = None

    def has_message(self):
        if self.session and session_keys['messages'] in self.session:
            return bool(self.session[session_keys['messages']])
        else:
            return False

    # category:['success','info', 'warning', 'danger']
    def add_message(self, category, message):
        item = {'category': category, 'message': message}
        if session_keys['messages'] in self.session and \
                isinstance(self.session[session_keys['messages']], dict):
            self.session[session_keys['messages']].append(item)
        else:
            self.session[session_keys['messages']] = [item]
        self.save_session()

    def read_messages(self):
        if session_keys['messages'] in self.session:
            all_messages = self.session.pop(session_keys['messages'], None)
            self.save_session()
            return all_messages
        return None

    def write_json(self, json):
        self.set_header("Content-Type", "application/json; charset=UTF-8")
        self.write(json)

    def write_error(self, status_code, **kwargs):
        if not config['debug']:
            if status_code == 403:
                self.render("403.html")
            elif status_code == 404 or 405:
                self.render("404.html")
            elif status_code == 500:
                self.render("500.html")
        if not self._finished:
            super(BaseHandler, self).write_error(status_code, **kwargs)

    def get_gravatar_url(self, email, default=None, size=40):
        body = {'s': str(size)}
        if default:
            body["d"] = default;
        elif config['default_avatar_url']:
            body["d"] = config['default_avatar_url']
        gravatar_url = "https://www.gravatar.com/avatar/" + hashlib.md5(email.lower()).hexdigest() + "?"
        gravatar_url += urllib.urlencode(body)
        return gravatar_url

    @gen.coroutine
    def on_finish(self):
        if self.db_session:
            self.db_session.close()
            # print "db_info:", self.application.db_pool.kw['bind'].pool.status()
        if self.session is not None and self.session_save_tag:
            yield self.session.save(self.session_expire_time)



================================================
FILE: controller/home.py
================================================
# coding=utf-8
from tornado import gen

from base import BaseHandler
from admin_article import ArticleAndCommentsFlush
from model.pager import Pager
from model.constants import Constants
from model.search_params.article_params import ArticleSearchParams
from model.search_params.comment_params import CommentSearchParams
from service.user_service import UserService
from service.article_service import ArticleService
from service.comment_service import CommentService


class HomeHandler(BaseHandler):
    @gen.coroutine
    def get(self):
        pager = Pager(self)
        article_search_params = ArticleSearchParams(self)
        article_search_params.show_article_type = True
        article_search_params.show_source = True
        article_search_params.show_summary = True
        article_search_params.show_comments_count = True
        pager = yield self.async_do(ArticleService.page_articles, self.db, pager, article_search_params)
        self.render("index.html", base_url=self.reverse_url('index'),
                    pager=pager, article_search_params=article_search_params)


class ArticleHandler(BaseHandler):
    @gen.coroutine
    def get(self, article_id):
        article = yield self.async_do(ArticleService.get_article_all, self.db, article_id, True, add_view_count=1)
        if article:
            comments_pager = Pager(self)
            comment_search_params = CommentSearchParams(self)
            comment_search_params.article_id = article_id
            comments_pager = yield self.async_do(CommentService.page_comments, self.db, comments_pager, comment_search_params)
            self.render("article_detials.html", article=article, comments_pager=comments_pager)
        else:
            self.write_error(404)


class ArticleCommentHandler(BaseHandler, ArticleAndCommentsFlush):
    @gen.coroutine
    def post(self, article_id):
        comment = dict(
            content=self.get_argument('content'),
            author_name=self.get_argument('author_name'),
            author_email=self.get_argument('author_email'),
            article_id=article_id,
            comment_type=self.get_argument('comment_type', None),
            rank=Constants.COMMENT_RANK_ADMIN if self.current_user else Constants.COMMENT_RANK_NORMAL,
            reply_to_id=self.get_argument('reply_to_id', None),
            reply_to_floor=self.get_argument('reply_to_floor', None),
        )
        comment_saved = yield self.async_do(CommentService.add_comment, self.db, article_id, comment)
        if comment_saved:
            yield self.flush_comments_cache(Constants.FLUSH_COMMENT_ACTION_ADD, comment_saved)
            self.add_message('success', u'评论成功')
        else:
            self.add_message('danger', u'评论失败')
        next_url = self.get_argument('next', None)
        if next_url:
            self.redirect(next_url)
        else:
            self.redirect(self.reverse_url('article', article_id)+"?pageNo=-1#comments")


class ArticleTypeHandler(BaseHandler):
    @gen.coroutine
    def get(self, type_id):
        pager = Pager(self)
        article_search_params = ArticleSearchParams(self)
        article_search_params.show_article_type=True
        article_search_params.show_source=True
        article_search_params.show_summary=True
        article_search_params.show_comments_count = True
        article_search_params.articleType_id = type_id
        pager = yield self.async_do(ArticleService.page_articles, self.db, pager, article_search_params)
        self.render("index.html", base_url=self.reverse_url('articleType', type_id),
                    pager=pager, article_search_params=article_search_params)


class articleSourceHandler(BaseHandler):
    @gen.coroutine
    def get(self, source_id):
        pager = Pager(self)
        article_search_params = ArticleSearchParams(self)
        article_search_params.show_article_type=True
        article_search_params.show_source=True
        article_search_params.show_summary=True
        article_search_params.show_comments_count = True
        article_search_params.source_id = source_id
        pager = yield self.async_do(ArticleService.page_articles, self.db, pager, article_search_params)
        self.render("index.html", base_url=self.reverse_url('articleSource', source_id),
                    pager=pager, article_search_params=article_search_params)


class LoginHandler(BaseHandler):

    def get(self):
        next_url = self.get_argument('next', '/')
        self.render("auth/login.html", next_url=next_url)

    @gen.coroutine
    def post(self):
        username = self.get_argument('username')
        password = self.get_argument('password')
        next_url = self.get_argument('next', '/')
        user = yield self.async_do(UserService.get_user, self.db, username)
        if user is not None and user.password == password:
            self.save_login_user(user)
            self.add_message('success', u'登陆成功!欢迎回来,{0}!'.format(username))
            self.redirect(next_url)
        else:
            self.add_message('danger', u'登陆失败!用户名或密码错误,请重新登陆。')
            self.get()


class LogoutHandler(BaseHandler):

    def get(self):
        self.logout()
        self.add_message('success', u'您已退出登陆。')
        self.redirect("/")




================================================
FILE: controller/super.py
================================================
# coding=utf-8
from tornado import gen

from base import BaseHandler
from service.user_service import UserService


class SuperHandler(BaseHandler):
    @gen.coroutine
    def get(self):
        user_count = yield self.async_do(UserService.get_count, self.db)
        if not user_count:
            self.render("super/init.html")
        else:
            self.write_error(404)

    @gen.coroutine
    def post(self):
        user = dict(
            email=self.get_argument('email'),
            username=self.get_argument('username'),
            password=self.get_argument('password'),
        )
        user_saved = yield self.async_do(UserService.save_user, self.db, user)
        if user_saved and user_saved.id:
            self.add_message('success', u'创建成功!')
            self.redirect(self.reverse_url('login'))
        else:
            self.add_message('danger', u'创建失败!')
            self.redirect(self.reverse_url('super.init'))


================================================
FILE: docker/Dockerfile
================================================
FROM python:2.7.13-alpine
MAINTAINER xtg <imgamermhq@gmail.com>

#时区问题(alpine解决方案)
RUN apk update && apk add ca-certificates && \
    apk add tzdata && \
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

ENV SUPERVISOR_VERSION 3.3.1
ENV NGINX_VERSION 1.10.3

#安装nginx
RUN GPG_KEYS=B0F4253373F8F6F510D42178520A9993A1C052F8 \
  && CONFIG="\
    --prefix=/etc/nginx \
    --sbin-path=/usr/sbin/nginx \
    --modules-path=/usr/lib/nginx/modules \
    --conf-path=/etc/nginx/nginx.conf \
    --error-log-path=/var/log/nginx/error.log \
    --http-log-path=/var/log/nginx/access.log \
    --pid-path=/var/run/nginx.pid \
    --lock-path=/var/run/nginx.lock \
    --http-client-body-temp-path=/var/cache/nginx/client_temp \
    --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
    --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
    --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
    --http-scgi-temp-path=/var/cache/nginx/scgi_temp \
    --user=nginx \
    --group=nginx \
    --with-http_ssl_module \
    --with-http_realip_module \
    --with-http_addition_module \
    --with-http_sub_module \
    --with-http_dav_module \
    --with-http_flv_module \
    --with-http_mp4_module \
    --with-http_gunzip_module \
    --with-http_gzip_static_module \
    --with-http_random_index_module \
    --with-http_secure_link_module \
    --with-http_stub_status_module \
    --with-http_auth_request_module \
    --with-http_xslt_module=dynamic \
    --with-http_image_filter_module=dynamic \
    --with-http_geoip_module=dynamic \
    --with-http_perl_module=dynamic \
    --with-threads \
    --with-stream \
    --with-stream_ssl_module \
    --with-http_slice_module \
    --with-mail \
    --with-mail_ssl_module \
    --with-file-aio \
    --with-http_v2_module \
    --with-ipv6 \
  " \
  && addgroup -S nginx \
  && adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx \
  && apk add --no-cache --virtual .build-deps \
    gcc \
    libc-dev \
    make \
    openssl-dev \
    pcre-dev \
    zlib-dev \
    linux-headers \
    curl \
    gnupg \
    libxslt-dev \
    gd-dev \
    geoip-dev \
    perl-dev \
  && curl -fSL http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz -o nginx.tar.gz \
  && curl -fSL http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz.asc  -o nginx.tar.gz.asc \
  && export GNUPGHOME="$(mktemp -d)" \
  && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$GPG_KEYS" \
  && gpg --batch --verify nginx.tar.gz.asc nginx.tar.gz \
  && rm -r "$GNUPGHOME" nginx.tar.gz.asc \
  && mkdir -p /usr/src \
  && tar -zxC /usr/src -f nginx.tar.gz \
  && rm nginx.tar.gz \
  && cd /usr/src/nginx-$NGINX_VERSION \
  && ./configure $CONFIG --with-debug \
  && make -j$(getconf _NPROCESSORS_ONLN) \
  && mv objs/nginx objs/nginx-debug \
  && mv objs/ngx_http_xslt_filter_module.so objs/ngx_http_xslt_filter_module-debug.so \
  && mv objs/ngx_http_image_filter_module.so objs/ngx_http_image_filter_module-debug.so \
  && mv objs/ngx_http_geoip_module.so objs/ngx_http_geoip_module-debug.so \
  && mv objs/ngx_http_perl_module.so objs/ngx_http_perl_module-debug.so \
  && ./configure $CONFIG \
  && make -j$(getconf _NPROCESSORS_ONLN) \
  && make install \
  && rm -rf /etc/nginx/html/ \
  && mkdir /etc/nginx/conf.d/ \
  && mkdir -p /usr/share/nginx/html/ \
  && install -m644 html/index.html /usr/share/nginx/html/ \
  && install -m644 html/50x.html /usr/share/nginx/html/ \
  && install -m755 objs/nginx-debug /usr/sbin/nginx-debug \
  && install -m755 objs/ngx_http_xslt_filter_module-debug.so /usr/lib/nginx/modules/ngx_http_xslt_filter_module-debug.so \
  && install -m755 objs/ngx_http_image_filter_module-debug.so /usr/lib/nginx/modules/ngx_http_image_filter_module-debug.so \
  && install -m755 objs/ngx_http_geoip_module-debug.so /usr/lib/nginx/modules/ngx_http_geoip_module-debug.so \
  && install -m755 objs/ngx_http_perl_module-debug.so /usr/lib/nginx/modules/ngx_http_perl_module-debug.so \
  && ln -s ../../usr/lib/nginx/modules /etc/nginx/modules \
  && strip /usr/sbin/nginx* \
  && strip /usr/lib/nginx/modules/*.so \
  && rm -rf /usr/src/nginx-$NGINX_VERSION \
  \
  # Bring in gettext so we can get `envsubst`, then throw
  # the rest away. To do this, we need to install `gettext`
  # then move `envsubst` out of the way so `gettext` can
  # be deleted completely, then move `envsubst` back.
  && apk add --no-cache --virtual .gettext gettext \
  && mv /usr/bin/envsubst /tmp/ \
  \
  && runDeps="$( \
    scanelf --needed --nobanner /usr/sbin/nginx /usr/lib/nginx/modules/*.so /tmp/envsubst \
      | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
      | sort -u \
      | xargs -r apk info --installed \
      | sort -u \
  )" \
  && apk add --no-cache --virtual .nginx-rundeps $runDeps \
  && apk del .build-deps \
  && apk del .gettext \
  && mv /tmp/envsubst /usr/local/bin/ \
  \
  # forward request and error logs to docker log collector
  && ln -sf /dev/stdout /var/log/nginx/access.log \
  && ln -sf /dev/stderr /var/log/nginx/error.log

#安装
RUN pip install supervisor==${SUPERVISOR_VERSION}

#导入相关配置
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/supervisord.conf /etc/supervisord.conf
#copy项目代码
COPY . /home/xtg/blog-xtg

WORKDIR /home/xtg/blog-xtg
#安装项目依赖
RUN apk add --update --no-cache mariadb-client-libs \
	&& apk add --no-cahe --virtual .build-deps \
		mariadb-dev \
		gcc \
		musl-dev \
	&& pip install -r requirements.txt \
	&& apk del .build-deps

EXPOSE 80
VOLUME /home/xtg/blog-xtg/logs

COPY docker/entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
CMD ["upgradedb"]

================================================
FILE: docker/entrypoint.sh
================================================
#!/bin/sh
set -e

if [ "$1" == "upgradedb" ]
then
    python main.py upgradedb
fi
exec supervisord -n


================================================
FILE: docker/nginx.conf
================================================
#user  xtg;
worker_processes  2;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    use epoll;  
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    upstream backend {
        server 127.0.0.1:8001;
        server 127.0.0.1:8002;
    }

    server {
        listen       80;

        location /static/ {
            root      /home/xtg/blog-xtg;
            expires    1d;
        }

        location / {
            proxy_pass http://backend;    
            proxy_redirect off;    
            proxy_set_header Host $host;    
            proxy_set_header X-Real-IP $remote_addr;    
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    
            client_max_body_size 10m;    
            client_body_buffer_size 128k;    
            proxy_connect_timeout 90;    
            proxy_send_timeout 90;    
            proxy_read_timeout 90;    
            proxy_buffer_size 64k;    
            proxy_buffers 32 32k;    
            proxy_busy_buffers_size 128k;    
            proxy_temp_file_write_size 128k;   
        }
    }


}


================================================
FILE: docker/supervisord.conf
================================================
; Sample supervisor config file.
;
; For more information on the config file, please see:
; http://supervisord.org/configuration.html
;
; Note: shell expansion ("~" or "$HOME") is not supported.  Environment
; variables can be expanded using this syntax: "%(ENV_HOME)s".

[unix_http_server]
file=/tmp/supervisor.sock   ; (the path to the socket file)
;chmod=0700                 ; socket file mode (default 0700)
;chown=nobody:nogroup       ; socket file uid:gid owner
;username=user              ; (default is no username (open server))
;password=123               ; (default is no password (open server))

;[inet_http_server]         ; inet (TCP) server disabled by default
;port=cma01:9001            ; (ip_address:port specifier, *:port for all iface)
;username=user              ; (default is no username (open server))
;password=123               ; (default is no password (open server))

[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB        ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10           ; (num of main logfile rotation backups;default 10)
loglevel=info                ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false               ; (start in foreground if true;default false)
minfds=1024                  ; (min. avail startup file descriptors;default 1024)
minprocs=200                 ; (min. avail process descriptors;default 200)
;umask=022                   ; (process file creation umask;default 022)
;user=chrism                 ; (default is current user, required if root)
;identifier=supervisor       ; (supervisord identifier, default is 'supervisor')
;directory=/tmp              ; (default is not to cd during start)
;nocleanup=true              ; (don't clean up tempfiles at start;default false)
;childlogdir=/tmp            ; ('AUTO' child log dir, default $TEMP)
;environment=KEY="value"     ; (key value pairs to add to environment)
;strip_ansi=false            ; (strip ansi escape codes in logs; def. false)

; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket
;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket
;username=chris              ; should be same as http_username if set
;password=123                ; should be same as http_password if set
;prompt=mysupervisor         ; cmd line prompt (default "supervisor")
;history_file=~/.sc_history  ; use readline history if available

; The below sample program section shows all possible program subsection values,
; create one or more 'real' program: sections to be able to control them under
; supervisor.

;[program:theprogramname]
;command=/bin/cat              ; the program (relative uses PATH, can take args)
;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
;numprocs=1                    ; number of processes copies to start (def 1)
;directory=/tmp                ; directory to cwd to before exec (def no cwd)
;umask=022                     ; umask for process (default None)
;priority=999                  ; the relative start priority (default 999)
;autostart=true                ; start at supervisord start (default: true)
;autorestart=unexpected        ; whether/when to restart (default: unexpected)
;startsecs=1                   ; number of secs prog must stay running (def. 1)
;startretries=3                ; max ; of serial start failures (default 3)
;exitcodes=0,2                 ; 'expected' exit codes for process (default 0,2)
;stopsignal=QUIT               ; signal used to kill process (default TERM)
;stopwaitsecs=10               ; max num secs to wait b4 SIGKILL (default 10)
;stopasgroup=false             ; send stop signal to the UNIX process group (default false)
;killasgroup=false             ; SIGKILL the UNIX process group (def false)
;user=chrism                   ; setuid to this UNIX account to run the program
;redirect_stderr=true          ; redirect proc stderr to stdout (default false)
;stdout_logfile=/a/path        ; stdout log path, NONE for none; default AUTO
;stdout_logfile_maxbytes=1MB   ; max ; logfile bytes b4 rotation (default 50MB)
;stdout_logfile_backups=10     ; ; of stdout logfile backups (default 10)
;stdout_capture_maxbytes=1MB   ; number of bytes in 'capturemode' (default 0)
;stdout_events_enabled=false   ; emit events on stdout writes (default false)
;stderr_logfile=/a/path        ; stderr log path, NONE for none; default AUTO
;stderr_logfile_maxbytes=1MB   ; max ; logfile bytes b4 rotation (default 50MB)
;stderr_logfile_backups=10     ; ; of stderr logfile backups (default 10)
;stderr_capture_maxbytes=1MB   ; number of bytes in 'capturemode' (default 0)
;stderr_events_enabled=false   ; emit events on stderr writes (default false)
;environment=A="1",B="2"       ; process environment additions (def no adds)
;serverurl=AUTO                ; override serverurl computation (childutils)

[program:blog-master]
command=python /home/xtg/blog-xtg/main.py --master=true --port=8001
process_name=%(program_name)s
numprocs=1
user=root
autostart=true
autorestart=true
startsecs=5
startretries=3
priority=10
redirect_stderr=true
stdout_logfile=/home/xtg/blog-master.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stopasgroup=false


[program:blog-slave]
command=python /home/xtg/blog-xtg/main.py --master=false --port=8002
process_name=%(program_name)s
numprocs=1
user=root
autostart=true
autorestart=true
startsecs=5
startretries=3
priority=20
redirect_stderr=true
stdout_logfile=/home/xtg/blog-slave.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stopasgroup=false

[program:nginx]
command=nginx -g 'daemon off;'
process_name=%(program_name)s
numprocs=1
user=root
autostart=true
autorestart=true
startsecs=1
startretries=3
priority=50
redirect_stderr=true
stdout_logfile=/home/xtg/nginx.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stopasgroup=false

; The below sample eventlistener section shows all possible
; eventlistener subsection values, create one or more 'real'
; eventlistener: sections to be able to handle event notifications
; sent by supervisor.

;[eventlistener:theeventlistenername]
;command=/bin/eventlistener    ; the program (relative uses PATH, can take args)
;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
;numprocs=1                    ; number of processes copies to start (def 1)
;events=EVENT                  ; event notif. types to subscribe to (req'd)
;buffer_size=10                ; event buffer queue size (default 10)
;directory=/tmp                ; directory to cwd to before exec (def no cwd)
;umask=022                     ; umask for process (default None)
;priority=-1                   ; the relative start priority (default -1)
;autostart=true                ; start at supervisord start (default: true)
;autorestart=unexpected        ; whether/when to restart (default: unexpected)
;startsecs=1                   ; number of secs prog must stay running (def. 1)
;startretries=3                ; max ; of serial start failures (default 3)
;exitcodes=0,2                 ; 'expected' exit codes for process (default 0,2)
;stopsignal=QUIT               ; signal used to kill process (default TERM)
;stopwaitsecs=10               ; max num secs to wait b4 SIGKILL (default 10)
;stopasgroup=false             ; send stop signal to the UNIX process group (default false)
;killasgroup=false             ; SIGKILL the UNIX process group (def false)
;user=chrism                   ; setuid to this UNIX account to run the program
;redirect_stderr=true          ; redirect proc stderr to stdout (default false)
;stdout_logfile=/a/path        ; stdout log path, NONE for none; default AUTO
;stdout_logfile_maxbytes=1MB   ; max ; logfile bytes b4 rotation (default 50MB)
;stdout_logfile_backups=10     ; ; of stdout logfile backups (default 10)
;stdout_events_enabled=false   ; emit events on stdout writes (default false)
;stderr_logfile=/a/path        ; stderr log path, NONE for none; default AUTO
;stderr_logfile_maxbytes=1MB   ; max ; logfile bytes b4 rotation (default 50MB)
;stderr_logfile_backups        ; ; of stderr logfile backups (default 10)
;stderr_events_enabled=false   ; emit events on stderr writes (default false)
;environment=A="1",B="2"       ; process environment additions
;serverurl=AUTO                ; override serverurl computation (childutils)

; The below sample group section shows all possible group values,
; create one or more 'real' group: sections to create "heterogeneous"
; process groups.

;[group:thegroupname]
;programs=progname1,progname2  ; each refers to 'x' in [program:x] definitions
;priority=999                  ; the relative start priority (default 999)

; The [include] section can just contain the "files" setting.  This
; setting can list multiple files (separated by whitespace or
; newlines).  It can also contain wildcards.  The filenames are
; interpreted as relative to this file.  Included files *cannot*
; include files themselves.

;[include]
;files = relative/directory/*.ini


================================================
FILE: extends/__init__.py
================================================
# coding=utf-8


================================================
FILE: extends/cache_tornadis.py
================================================
# coding: utf-8
import logging

import tornadis
import tornado.gen

logger = logging.getLogger(__name__)


class CacheManager(object):
    def __init__(self, options):
        self.connection_pool = None
        self.options = options
        self.client = None

    def get_connection_pool(self):
        if not self.connection_pool:
            self.connection_pool = tornadis.ClientPool(host=self.options['host'],port=self.options['port'],
                                                       password=self.options['password'], db=self.options['db_no'],
                                                       max_size=self.options['max_connections'])
        return self.connection_pool

    @tornado.gen.coroutine
    def get_redis_client(self):
        connection_pool = self.get_connection_pool()
        with (yield connection_pool.connected_client()) as client:
            if isinstance(client, tornadis.TornadisException):
                logger.error(client.message)
            else:
                raise tornado.gen.Return(client)

    @tornado.gen.coroutine
    def fetch_client(self):
        self.client = yield self.get_redis_client()

    @tornado.gen.coroutine
    def call(self, *args, **kwargs):
        yield self.fetch_client()
        if self.client:
            reply = yield self.client.call(*args, **kwargs)
            if isinstance(reply, tornadis.TornadisException):
                logger.error(reply.message)
            else:
                raise tornado.gen.Return(reply)

    @tornado.gen.coroutine
    def call(self, *args, **kwargs):
        yield self.fetch_client()
        if self.client:
            reply = yield self.client.call(*args, **kwargs)
            if isinstance(reply, tornadis.TornadisException):
                logger.error(reply.message)
            else:
                raise tornado.gen.Return(reply)

    @tornado.gen.coroutine
    def call_watch_transaction(self, watch_key, *args, **kwargs):
        yield self.fetch_client()
        if self.client:
            while True:
                yield self.client.call("WATCH", watch_key)
                yield self.client.call("MULTI")
                yield self.client.call(*args, **kwargs)
                result = yield self.client.call("EXEC")
                if isinstance(result, tornadis.TornadisException):
                    logger.error(result.message)
                else:
                    raise tornado.gen.Return(result)



================================================
FILE: extends/pub_sub_tornadis.py
================================================
# coding=utf-8
import tornado.ioloop
import tornado.gen
import tornadis
import logging

logger = logging.getLogger(__name__)


class PubSubTornadis(object):

    def __init__(self, redis_pub_sub_config, loop=None):
        self.redis_pub_sub_config = redis_pub_sub_config
        if not loop:
            loop = tornado.ioloop.IOLoop.current()
        self.loop = loop
        self.autoconnect = self.redis_pub_sub_config['autoconnect']
        self.client = self.get_client()
        self.pub_client = None
        self.connect_times = 0
        self.max_connect_wait_time = 10

    def get_client(self):
        client = tornadis.PubSubClient(host=self.redis_pub_sub_config['host'], port=self.redis_pub_sub_config['port'],
                                       password=self.redis_pub_sub_config['password'],
                                       autoconnect=self.autoconnect)
        return client

    def get_pub_client(self):
        if not self.pub_client:
            self.pub_client = tornadis.Client(host=self.redis_pub_sub_config['host'],
                                              port=self.redis_pub_sub_config['port'],
                                              password=self.redis_pub_sub_config['password'],
                                              autoconnect=self.autoconnect)
        return self.pub_client

    @tornado.gen.coroutine
    def pub_call(self, msg, *channels):
        pub_client = self.get_pub_client()
        if not pub_client.is_connected():
            yield pub_client.connect()
        if not channels:
            channels = self.redis_pub_sub_config['channels']
        for channel in channels:
            yield pub_client.call("PUBLISH", channel, msg)

    def long_listen(self):
        self.loop.add_callback(self.connect_and_listen, self.redis_pub_sub_config['channels'])

    @tornado.gen.coroutine
    def connect_and_listen(self, channels):
        connected = yield self.client.connect()
        if connected:
            subscribed = yield self.client.pubsub_subscribe(*channels)
            if subscribed:
                self.connect_times = 0
                yield self.first_do_after_subscribed()
                while True:
                    msgs = yield self.client.pubsub_pop_message()
                    try:
                        yield self.do_msg(msgs)
                        if isinstance(msgs, tornadis.TornadisException):
                            # closed connection by the server
                            break
                    except Exception, e:
                        logger.exception(e)
            self.client.disconnect()
        if self.autoconnect:
            wait_time = self.connect_times \
                if self.connect_times < self.max_connect_wait_time else self.max_connect_wait_time
            logger.warn("等待{}s,重新连接redis消息订阅服务".format(wait_time))
            yield tornado.gen.sleep(wait_time)
            self.long_listen()
            self.connect_times += 1

    # override
    @tornado.gen.coroutine
    def first_do_after_subscribed(self):
        logger.info("订阅成功")

    # override
    @tornado.gen.coroutine
    def do_msg(self, msgs):
        logger.info("收到订阅消息"+ str(msgs))


================================================
FILE: extends/session_redis.py
================================================
# coding: utf-8
import uuid
import json
import redis


# 同步的redis客户端实现,不适用tornado,暂时弃用.
class Session(dict):
    def __init__(self, request_handler):
        super(Session, self).__init__()
        self.session_id = None
        self.session_manager = request_handler.application.session_manager
        self.request_handler = request_handler
        self.client = self.session_manager.get_redis_client()
        self.fetch_client()

    def get_session_id(self):
        if not self.session_id:
            self.session_id = self.request_handler.get_secure_cookie(self.session_manager.session_key_name)
        return self.session_id

    def generate_session_id(self):
        if not self.get_session_id():
            self.session_id = str(uuid.uuid1())
        return self.session_id

    def fetch_client(self):
        if self.get_session_id():
            data = self.client.get(self.session_id)
            if data:
                self.update(json.loads(data))

    def save(self):
        session_id = self.generate_session_id()
        data_json = json.dumps(self)
        self.client.set(session_id, data_json)
        self.request_handler.set_secure_cookie(self.session_manager.session_key_name, session_id,
                                               expires_days=self.session_manager.session_expires_days)


class SessionManager(object):
    def __init__(self, options):
        self.connection_pool = None
        self.options = options
        self.session_key_name = options['session_key_name']
        self.session_expires_days = options['session_expires_days']

    def get_connection_pool(self):
        if not self.connection_pool:
            self.connection_pool = redis.ConnectionPool(host=self.options['host'],port=self.options['port'],
                                                        db=self.options['db_no'],password=self.options['password'],
                                                        max_connections=self.options['max_connections'])
        return self.connection_pool

    def get_redis_client(self):
        connection_pool = self.get_connection_pool()
        return redis.Redis(connection_pool=connection_pool)

================================================
FILE: extends/session_tornadis.py
================================================
# coding: utf-8
import uuid
import json
import tornadis
import tornado.gen
import logging

logger = logging.getLogger(__name__)


class Session(dict):
    def __init__(self, request_handler):
        super(Session, self).__init__()
        self.session_id = None
        self.session_manager = request_handler.application.session_manager
        self.request_handler = request_handler
        self.client = None

    @tornado.gen.coroutine
    def init_fetch(self):
        self.client = yield self.session_manager.get_redis_client()
        yield self.fetch_client()

    def get_session_id(self):
        if not self.session_id:
            self.session_id = self.request_handler.get_secure_cookie(self.session_manager.session_key_name)
        return self.session_id

    def generate_session_id(self):
        if not self.get_session_id():
            self.session_id = str(uuid.uuid1())
            self.request_handler.set_secure_cookie(self.session_manager.session_key_name, self.session_id,
                                                   expires_days=self.session_manager.session_expires_days)
        return self.session_id

    @tornado.gen.coroutine
    def fetch_client(self):
        if self.get_session_id():
            data = yield self.call_client("GET", self.session_id)
            if data:
                self.update(json.loads(data))

    @tornado.gen.coroutine
    def save(self, expire_time=None):
        session_id = self.generate_session_id()
        data_json = json.dumps(self)
        yield self.call_client("SET", session_id, data_json)
        if expire_time:
            yield self.call_client("EXPIRE", session_id, expire_time)

    @tornado.gen.coroutine
    def call_client(self, *args, **kwargs):
        if self.client:
            reply = yield self.client.call(*args, **kwargs)
            if isinstance(reply, tornadis.TornadisException):
                logger.error(reply.message)
            else:
                raise tornado.gen.Return(reply)


class SessionManager(object):
    def __init__(self, options):
        self.connection_pool = None
        self.options = options
        self.session_key_name = options['session_key_name']
        self.session_expires_days = options['session_expires_days']

    def get_connection_pool(self):
        if not self.connection_pool:
            self.connection_pool = tornadis.ClientPool(host=self.options['host'],port=self.options['port'],
                                                       password=self.options['password'], db=self.options['db_no'],
                                                       max_size=self.options['max_connections'])
        return self.connection_pool

    @tornado.gen.coroutine
    def get_redis_client(self):
        connection_pool = self.get_connection_pool()
        with (yield connection_pool.connected_client()) as client:
            if isinstance(client, tornadis.TornadisException):
                logger.error(client.message)
            else:
                raise tornado.gen.Return(client)


================================================
FILE: extends/time_task.py
================================================
# coding=utf-8
import logging
from apscheduler.schedulers.tornado import TornadoScheduler

logger = logging.getLogger(__name__)


class TimeTask(object):
    def __init__(self, sqlalchemy_engine):
        self.scheduler = TornadoScheduler()
        self.scheduler.add_jobstore("sqlalchemy", engine=sqlalchemy_engine)

    def add_cache_flush_task(self, func, *args, **kwargs):
        self.scheduler.add_job(func, 'cron', args=args, kwargs=kwargs,
                               id="cache_flush", replace_existing=True, hour=0, day='*')
        return self

    def start_tasks(self):
        self.scheduler.start()


================================================
FILE: extends/utils.py
================================================
# coding=utf-8
import json
import logging
from sqlalchemy.ext.declarative import DeclarativeMeta

logger = logging.getLogger(__name__)


def singleton(cls, *args, **kw):
    instances = {}

    def _singleton():
        if cls not in instances:
            instances[cls] = cls(*args, **kw)
        return instances[cls]
    return _singleton


class AlchemyEncoder(json.JSONEncoder):

    def __init__(self, dumps_objs=None, *w, **kw):
        super(AlchemyEncoder, self).__init__(*w, **kw)
        if dumps_objs is None:
            dumps_objs = []
        self.dumps_objs = dumps_objs

    def default(self, o):
        if isinstance(o.__class__, DeclarativeMeta):
            self.dumps_objs.append(o)
            data = {}
            fields = o.__json__() if hasattr(o, '__json__') else dir(o)
            fields = [f for f in fields if not f.startswith('_') and f not in ['metadata', 'query', 'query_class']]
            for field in fields:
                value = o.__getattribute__(field)
                if value and self.dumps_objs and value in self.dumps_objs:
                    continue
                try:
                    json.dumps(value, cls=AlchemyEncoder, dumps_objs=self.dumps_objs)
                    data[field] = value
                except TypeError:
                    pass
            return data
        return json.JSONEncoder.default(self, o)


#  可通过 .attr 访问的dict
class Dict(dict):
    def __getattr__(self, key):
        try:
            if isinstance(self[key], dict):
                return Dict(self[key])
            return self[key]
        except KeyError:
            logger.warning(key+" not in "+str(self))
            return None;

    def __setattr__(self, key, value):
        self[key] = value


================================================
FILE: log_config.py
================================================
# coding=utf-8
import logging
import logging.handlers
import tornado.log

FILE = dict(
    log_path="logs/log", # 末尾自动添加 @端口号.txt_日期
    when="D", # 以什么单位分割文件
    interval=1, # 以上面的时间单位,隔几个单位分割文件
    backupCount=30, # 保留多少历史记录文件
    fmt="%(asctime)s - %(name)s - %(filename)s[line:%(lineno)d] - %(levelname)s - %(message)s",
)


def init(port, console_handler=False, file_handler=True, log_path=FILE['log_path'], base_level="INFO"):
    logger = logging.getLogger()
    logger.setLevel(base_level)
    # 配置控制台输出
    if console_handler:
        channel_console = logging.StreamHandler()
        channel_console.setFormatter(tornado.log.LogFormatter())
        logger.addHandler(channel_console)
    # 配置文件输出
    if file_handler:
        if not log_path:
            log_path = FILE['log_path']
        log_path = log_path+"@"+str(port)+".txt"
        formatter = logging.Formatter(FILE['fmt']);
        channel_file = logging.handlers.TimedRotatingFileHandler(
            filename=log_path,
            when=FILE['when'],
            interval=FILE['interval'],
            backupCount=FILE['backupCount'])
        channel_file.setFormatter(formatter)
        logger.addHandler(channel_file)

================================================
FILE: main.py
================================================
# coding=utf-8
import os, sys

import concurrent.futures
import tornado.ioloop
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from tornado.options import options

import log_config
from config import config, redis_pub_sub_config, site_cache_config, redis_session_config
from controller.base import BaseHandler
from extends.cache_tornadis import CacheManager
from extends.session_tornadis import SessionManager
from service.init_service import flush_all_cache
from service.pubsub_service import PubSubService
from url_mapping import handlers

# tornado server相关参数
settings = dict(
    template_path=os.path.join(os.path.dirname(__file__), "template"),
    static_path=os.path.join(os.path.dirname(__file__), "static"),
    compress_response=config['compress_response'],
    xsrf_cookies=config['xsrf_cookies'],
    cookie_secret=config['cookie_secret'],
    login_url=config['login_url'],
    debug=config['debug'],
    default_handler_class=BaseHandler,
)


# sqlalchemy连接池配置以及生成链接池工厂实例
def db_poll_init():
    engine_config = config['database']['engine_url']
    engine = create_engine(engine_config, **config['database']["engine_setting"])
    config['database']['engine'] = engine
    db_poll = sessionmaker(bind=engine)
    return db_poll


def cache_manager_init():
    cache_manager = CacheManager(site_cache_config)
    return cache_manager


# 继承tornado.web.Application类,可以在构造函数里做站点初始化(初始数据库连接池,初始站点配置,初始异步线程池,加载站点缓存等)
class Application(tornado.web.Application):
    def __init__(self):
        super(Application, self).__init__(handlers, **settings)
        self.session_manager = SessionManager(config['redis_session'])
        self.thread_executor = concurrent.futures.ThreadPoolExecutor(config['max_threads_num'])
        self.db_pool = db_poll_init()
        self.cache_manager = cache_manager_init()
        self.pubsub_manager = None


#  从命令行读取配置,如果这些参数不传,默认使用config.py的配置项
def parse_command_line():
    options.define("port", help="run server on a specific port", type=int)
    options.define("log_console", help="print log to console", type=bool)
    options.define("log_file", help="print log to file", type=bool)
    options.define("log_file_path", help="path of log_file", type=str)
    options.define("log_level", help="level of logging", type=str)
    # 集群中最好有且仅有一个实例为master,一般用于执行全局的定时任务
    options.define("master", help="is master node? (true:master / false:slave)", type=bool)
    # sqlalchemy engine_url, 例如pgsql 'postgresql+psycopg2://mhq:1qaz2wsx@localhost:5432/blog'
    options.define("engine_url", help="engine_url for sqlalchemy", type=str)
    # redis相关配置, 覆盖所有用到redis位置的配置
    options.define("redis_host", help="redis host e.g 127.0.0.1", type=str)
    options.define("redis_port", help="redis port e.g 6379", type=int)
    options.define("redis_password", help="redis password set this option if has pwd ", type=str)
    options.define("redis_db", help="redis db e.g 0", type=int)

    # 读取 项目启动时,命令行上添加的参数项
    options.logging = None  # 不用tornado自带的logging配置
    options.parse_command_line()
    # 覆盖默认的config配置
    if options.port is not None:
        config['port'] = options.port
    if options.log_console is not None:
        config['log_console'] = options.log_console
    if options.log_file is not None:
        config['log_file'] = options.log_file
    if options.log_file_path is not None:
        config['log_file_path'] = options.log_file_path
    if options.log_level is not None:
        config['log_level'] = options.log_level
    if options.master is not None:
        config['master'] = options.master
    if options.engine_url is not None:
        config['database']['engine_url'] = options.engine_url
    if options.redis_host is not None:
        redis_session_config['host'] = options.redis_host
        site_cache_config['host'] = options.redis_host
        redis_pub_sub_config['host'] = options.redis_host
    if options.redis_port is not None:
        redis_session_config['port'] = options.redis_port
        site_cache_config['port'] = options.redis_port
        redis_pub_sub_config['port'] = options.redis_port
    if options.redis_password is not None:
        redis_session_config['password'] = options.redis_password
        site_cache_config['password'] = options.redis_password
        redis_pub_sub_config['password'] = options.redis_password
    if options.redis_db is not None:
        redis_session_config['db_no'] = options.redis_db
        site_cache_config['db_no'] = options.redis_db


if __name__ == '__main__':
    if len(sys.argv) >= 2:
        if sys.argv[1] == 'upgradedb':
            # 更新数据库结构,初次获取或更新版本后调用一次python main.py upgradedb即可
            from alembic.config import main
            main("upgrade head".split(' '), 'alembic')
            exit(0)
    # 加载命令行配置
    parse_command_line()
    # 加载日志管理
    log_config.init(config['port'], config['log_console'],
                    config['log_file'], config['log_file_path'], config['log_level'])
    # 创建application
    application = Application()
    application.listen(config['port'])
    # 全局注册application
    config['application'] = application
    loop = tornado.ioloop.IOLoop.current()
    # 加载redis消息监听客户端
    pubsub_manager = PubSubService(redis_pub_sub_config, application, loop)
    pubsub_manager.long_listen()
    application.pubsub_manager = pubsub_manager
    # 为master节点注册定时任务
    if config['master']:
        from extends.time_task import TimeTask
        TimeTask(config['database']['engine']).add_cache_flush_task(flush_all_cache).start_tasks()
    loop.start()


================================================
FILE: model/__init__.py
================================================
# coding=utf-8


================================================
FILE: model/constants.py
================================================
# coding=utf-8


class Constants(object):
    SYSTEM_PLUGIN = "system_plugin"

    COMMENT_RANK_ADMIN = "admin"
    COMMENT_RANK_NORMAL = "normal"
    COMMENT_TYPE_COMMENT = "comment"
    COMMENT_TYPE_REPLY = "reply"

    FLUSH_ARTICLE_ACTION_ADD = "add"
    FLUSH_ARTICLE_ACTION_UPDATE = "update"
    FLUSH_ARTICLE_ACTION_REMOVE = "remove"

    FLUSH_COMMENT_ACTION_ADD = "add"
    FLUSH_COMMENT_ACTION_UPDATE = "update"
    FLUSH_COMMENT_ACTION_REMOVE = "remove"

    ARTICLE_TYPE_DEFAULT_ID = 1


================================================
FILE: model/logined_user.py
================================================
# coding=utf-8
from extends.utils import Dict


class LoginUser(Dict):
        # self['id'] = None
        # self['name'] = None
        # self['avatar'] = None
        # self['email'] = None

    def __init__(self, user):
        super(LoginUser, self).__init__()
        if isinstance(user, dict):
            self.update(user)

    # def add_message(self, message):
    #     if 'messages' not in self:
    #         self['messages'] = [message]
    #     else:
    #         self['messages'].append(message)
    #
    # def read_messages(self):
    #     all_messages = self['messages']
    #     self['messages'] = None
    #     return all_messages


================================================
FILE: model/models.py
================================================
# coding: utf-8
from datetime import datetime
from model.constants import Constants
from sqlalchemy.orm import contains_eager, deferred
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, DateTime, Integer, String, Boolean, Text, ForeignKey, BigInteger, DATE
from sqlalchemy.orm import relationship, backref
DbBase = declarative_base()


class DbInit(object):
    created_at = Column(DateTime, default=datetime.now)


class User(DbBase,DbInit):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    email = Column(String(64), unique=True, index=True)
    username = Column(String(64), unique=True, index=True)
    password = Column(String(128))

    def verify_password(self, password):
        return self.password == password


class Menu(DbBase):
    __tablename__ = 'menus'
    id = Column(Integer, primary_key=True)
    name = Column(String(64), unique=True)
    types = relationship('ArticleType', backref='menu', lazy='dynamic')
    order = Column(Integer, default=0, nullable=False)

    def fetch_all_types(self, only_show_not_hide=False):
        query = self.types
        if only_show_not_hide:
            query = query.join(ArticleType.setting). \
                filter(ArticleTypeSetting.hide.isnot(True)). \
                options(contains_eager(ArticleType.setting))
        self.all_types = query.all()

    def __repr__(self):
        return '<Menu %r>' % self.name


class ArticleTypeSetting(DbBase):
    __tablename__ = 'articleTypeSettings'
    id = Column(Integer, primary_key=True)
    name = Column(String(64), unique=True)
    protected = Column(Boolean, default=False)
    hide = Column(Boolean, default=False)
    types = relationship('ArticleType', backref='setting', lazy='dynamic')

    @staticmethod
    def return_setting_hide():
        return [(2, u'公开'), (1, u'隐藏')]

    def __repr__(self):
        return '<ArticleTypeSetting %r>' % self.name


class ArticleType(DbBase):
    __tablename__ = 'articleTypes'
    id = Column(Integer, primary_key=True)
    name = Column(String(64), unique=True)
    introduction = Column(Text, default=None)
    articles = relationship('Article', backref='articleType', lazy='dynamic')
    menu_id = Column(Integer, ForeignKey('menus.id'), default=None)
    setting_id = Column(Integer, ForeignKey('articleTypeSettings.id'))

    @property
    def is_protected(self):
        if self.setting:
            return self.setting.protected
        else:
            return False

    @property
    def is_hide(self):
        if self.setting:
            return self.setting.hide
        else:
            return False

    def fetch_articles_count(self):
        self.articles_count = self.articles.count()
    # if the articleType does not have setting,
    # its is_hie and is_protected property will be False.

    def __repr__(self):
        return '<Type %r>' % self.name


class Source(DbBase):
    __tablename__ = 'sources'
    id = Column(Integer, primary_key=True)
    name = Column(String(64), unique=True)
    articles = relationship('Article', backref='source', lazy='dynamic')

    def fetch_articles_count(self):
        self.articles_count = self.articles.count()

    def __repr__(self):
        return '<Source %r>' % self.name


class Comment(DbBase):
    __tablename__ = 'comments'
    id = Column(Integer, primary_key=True)
    content = Column(Text)
    create_time = Column(DateTime, default=datetime.now)
    author_name = Column(String(64))
    author_email = Column(String(64))
    article_id = Column(Integer, ForeignKey('articles.id'))
    disabled = Column(Boolean, default=False)
    comment_type = Column(String(64), default=Constants.COMMENT_TYPE_COMMENT)
    rank = Column(String(64), name='rk', default=Constants.COMMENT_RANK_NORMAL)
    floor = Column(Integer, nullable=False)
    reply_to_id = Column(Integer)
    reply_to_floor = Column(String(64))


class Article(DbBase):
    __tablename__ = 'articles'
    id = Column(Integer, primary_key=True)
    title = Column(String(64))
    content = deferred(Column(Text))  # 延迟加载,避免在列表查询时查询该字段
    summary = deferred(Column(Text))  # 延迟加载,避免在列表查询时查询该字段
    create_time = Column(DateTime, index=True, default=datetime.now)
    update_time = deferred(Column(DateTime, index=True, default=datetime.now, onupdate=datetime.now))
    num_of_view = Column(Integer, default=0)
    articleType_id = Column(Integer, ForeignKey('articleTypes.id'))
    source_id = Column(Integer, ForeignKey('sources.id'))
    comments = relationship('Comment', backref='article', lazy='dynamic')

    def fetch_comments_count(self, count=None):
        self.comments_count = count if count is not None else self.comments.count()

    def __repr__(self):
        return '<Article %r>' % self.title


class BlogInfo(DbBase):
    __tablename__ = 'blog_info'
    id = Column(Integer, primary_key=True)
    title = Column(String(64))
    signature = Column(Text)
    navbar = Column(String(64))


class Plugin(DbBase):
    __tablename__ = 'plugins'
    id = Column(Integer, primary_key=True)
    title = Column(String(64), unique=True)
    note = Column(Text, default='')
    content = Column(Text, default='')
    order = Column(Integer, index=True, default=0)
    disabled = Column(Boolean, default=False)

    def __repr__(self):
        return '<Plugin %r>' % self.title


class BlogView(DbBase):
    __tablename__ = 'blog_view'
    date = Column(DATE, primary_key=True)
    pv = Column(BigInteger, default=0)
    uv = Column(BigInteger, default=0)


================================================
FILE: model/pager.py
================================================
# coding=utf-8
from extends.utils import Dict


class Pager(Dict):

    DEFAULT_PAGE_SIZE = 10

    def __init__(self, request):
        self.pageNo = int(request.get_argument("pageNo", 1))
        self.pageSize = int(request.get_argument("pageSize", Pager.DEFAULT_PAGE_SIZE))
        self.totalPage = 1
        self.totalCount = 0
        self.result = []

    def build_query(self, query):
        limit = self.pageSize
        if self.pageNo < 0:
            self.pageNo = self.pageNo + self.totalPage + 1
        offset = (self.pageNo-1)*self.pageSize if self.pageNo > 0 else 0
        query = query.limit(limit).offset(offset)
        return query

    def set_total_count(self, count):
        self.totalCount = count
        if count > 0:
            self.totalPage = (count+self.pageSize-1) / self.pageSize

    def set_result(self, result):
        if result:
            self.result = result

    def has_prev(self):
        return self.pageNo > 1

    def has_next(self):
        return self.pageNo < self.totalPage

    def build_url(self, url, page_no, params):
        if '?' in url:
            parts = url.split('?', 1)
            url = parts[0]
            params = parts[1]+"&"+params
        if page_no < 1:
            page_no = 0
        if page_no > self.totalPage:
            page_no = self.totalPage
        url = "{0}?pageNo={1}".format(url, page_no)
        if self.pageSize != Pager.DEFAULT_PAGE_SIZE:
            url += "&pageSize={0}".format(self.pageSize)
        if params:
            if params.startswith("#"):
                url += params
            else:
                url += "&{0}".format(params)
        return url


================================================
FILE: model/search_params/__init__.py
================================================
# coding=utf-8


================================================
FILE: model/search_params/article_params.py
================================================
# coding=utf-8
class ArticleSearchParams(object):

    ORDER_MODE_CREATE_TIME_DESC = 1

    def __init__(self, request):
        self.order_mode = request.get_argument("order_mode", ArticleSearchParams.ORDER_MODE_CREATE_TIME_DESC)
        self.source_id = request.get_argument("source_id", None)
        self.articleType_id = request.get_argument("articleType_id", None)
        self.show_source = True
        self.show_article_type = True
        self.show_summary = False
        self.show_content = False
        self.show_comments_count = False

    def to_url_params(self):
        s = ""
        if self.source_id:
            s = "source_id={0}".format(self.source_id)
        if self.articleType_id:
            if s:
                s += "&"
            s += "articleType_id={0}".format(self.articleType_id)
        return s

================================================
FILE: model/search_params/article_type_params.py
================================================
# coding=utf-8
class ArticleTypeSearchParams(object):

    ORDER_MODE_ID_DESC = 1

    def __init__(self, request):
        self.order_mode = request.get_argument("order_mode", ArticleTypeSearchParams.ORDER_MODE_ID_DESC)
        self.show_setting = False
        self.show_articles_count = False


================================================
FILE: model/search_params/comment_params.py
================================================
# coding=utf-8
class CommentSearchParams(object):

    ORDER_MODE_CREATE_TIME_ASC = 1
    ORDER_MODE_CREATE_TIME_DESC = 2

    def __init__(self, request):
        self.order_mode = request.get_argument("order_mode", CommentSearchParams.ORDER_MODE_CREATE_TIME_ASC)
        self.article_id = request.get_argument("article_id", None)
        self.show_article_id_title = False


================================================
FILE: model/search_params/menu_params.py
================================================
# coding=utf-8
class MenuSearchParams(object):

    ORDER_MODE_ORDER_ASC = 1

    def __init__(self, request):
        self.order_mode = request.get_argument("order_mode", MenuSearchParams.ORDER_MODE_ORDER_ASC)


================================================
FILE: model/search_params/plugin_params.py
================================================
# coding=utf-8


class PluginSearchParams(object):

    ORDER_MODE_ORDER_ASC = 1

    def __init__(self, request):
        self.order_mode = request.get_argument("order_mode", PluginSearchParams.ORDER_MODE_ORDER_ASC)


================================================
FILE: model/site_info.py
================================================
# coding=utf-8


class SiteCollection(object):
    title = None                # string
    signature = None            # string
    navbar = None               # string
    menus = None                # json(list)
    article_types_not_under_menu = None # 不在menu下的article_types     #json(list)
    plugins = None              # JSON(list)
    pv = None                   # int
    uv = None                   # int
    article_count = None        # int
    comment_count = None        # int
    article_sources = None      # JSON(list)


================================================
FILE: requirements.txt
================================================
tornado==4.4.2
sqlalchemy==1.0.15
tornadis==0.8.0
futures==3.0.5
alembic==0.9.1
apscheduler==3.3.1
mysql-connector-python==8.0.23

================================================
FILE: service/__init__.py
================================================
# coding=utf-8


class BaseService(object):
    @staticmethod
    def query_pager(query, pager, count=None):
        if count:
            pager.set_total_count(count)
        else:
            pager.set_total_count(query.count())
        query_result = pager.build_query(query)
        pager.set_result(query_result.all())
        return pager


================================================
FILE: service/article_service.py
================================================
# coding=utf-8
import logging
import re

from model.site_info import SiteCollection
from sqlalchemy.orm import joinedload, undefer
from model.models import Article, Source
from model.constants import Constants
from model.search_params.article_params import ArticleSearchParams
from . import BaseService
from comment_service import CommentService

logger = logging.getLogger(__name__)


class ArticleService(object):
    MARKDOWN_REG = "[\\\`\*\_\[\]\#\+\-\!\>\s]";
    SUMMARY_LIMIT = 120;

    @staticmethod
    def get_article_all(db_session, article_id, show_source_type=False, add_view_count=None):
        query = db_session.query(Article);
        if show_source_type:
            query = query.options(joinedload(Article.source)).\
                options(joinedload(Article.articleType).load_only("id", "name"))
        article = query.options(undefer(Article.summary), undefer(Article.content), undefer(Article.update_time)).\
            get(article_id)
        if article and add_view_count:
            article.num_of_view = Article.num_of_view + add_view_count
            db_session.commit()
        return article

    @staticmethod
    def page_articles(db_session, pager, search_params):
        query = db_session.query(Article)
        count = SiteCollection.article_count
        if search_params:
            if search_params.show_comments_count:
                stmt = CommentService.get_comments_count_subquery(db_session)
                query = db_session.query(Article, stmt.c.comments_count).\
                    outerjoin(stmt, Article.id == stmt.c.article_id)
            if search_params.show_summary:
                query = query.options(undefer(Article.summary))
            if search_params.show_content:
                query = query.options(undefer(Article.content))
            if search_params.show_source:
                query = query.options(joinedload(Article.source))
            if search_params.show_article_type:
                query = query.options(joinedload(Article.articleType).load_only("id", "name"))
            if search_params.order_mode == ArticleSearchParams.ORDER_MODE_CREATE_TIME_DESC:
                query = query.order_by(Article.create_time.desc())
            if search_params.source_id:
                count = None
                query = query.filter(Article.source_id == search_params.source_id)
            if search_params.articleType_id:
                count = None
                query = query.filter(Article.articleType_id == search_params.articleType_id)
        pager = BaseService.query_pager(query, pager, count)
        if pager.result:
            if search_params.show_comments_count:
                result = []
                for article, comments_count in pager.result:
                    article.fetch_comments_count(comments_count if comments_count else 0)
                    result.append(article)
                pager.result = result
        return pager

    @staticmethod
    def add_article(db_session, article):
        try:
            summary = article["summary"].strip() if article["summary"] else None
            if not summary:
                summary = ArticleService.get_core_content(article["content"], ArticleService.SUMMARY_LIMIT)
            article_to_add = Article(title=article["title"], content=article["content"],
                                     summary=summary, articleType_id=article["articleType_id"],
                                     source_id=article["source_id"])
            db_session.add(article_to_add)
            db_session.commit()
            return article_to_add
        except Exception, e:
            logger.exception(e)
        return None

    @staticmethod
    def update_article(db_session, article):
        try:
            summary = article["summary"].strip() if article["summary"] else None
            if not summary:
                summary = ArticleService.get_core_content(article["content"], ArticleService.SUMMARY_LIMIT)
            article_to_update = ArticleService.get_article_all(db_session, article["id"])
            article_old = Article(title=article_to_update.title, content=article_to_update.content,
                                  summary=article_to_update.summary, articleType_id=article_to_update.articleType_id,
                                  source_id=article_to_update.source_id)
            article_to_update.title = article["title"]
            article_to_update.content = article["content"]
            article_to_update.summary = summary
            article_to_update.articleType_id = int(article["articleType_id"]) if article["articleType_id"] else None
            article_to_update.source_id = int(article["source_id"]) if article["source_id"] else None
            db_session.commit()
            return article_to_update, article_old
        except Exception, e:
            logger.exception(e)
        return None

    @staticmethod
    def delete_article(db_session, article_id):
        try:
            article = db_session.query(Article).get(article_id)
            if article:
                comments_deleted = CommentService.remove_by_article_id(db_session, article_id, False)
                db_session.delete(article)
                db_session.commit()
            return article, comments_deleted;
        except Exception, e:
            logger.exception(e)
        return None

    @staticmethod
    def get_core_content(content, limit=0):
        core_content = re.sub(ArticleService.MARKDOWN_REG, '', content)
        if limit > 0:
            return core_content[:limit]
        return core_content

    @staticmethod
    def get_count(db_session):
        article_count = db_session.query(Article).count()
        return article_count

    # article_sources
    @staticmethod
    def get_article_sources(db_session):
        article_sources = db_session.query(Source).all()
        if article_sources:
            for source in article_sources:
                source.fetch_articles_count()
        return article_sources

    @staticmethod
    def set_article_type_default_by_article_type_id(db_session, article_type_id, auto_commit=True):
        try:
            db_session.query(Article).filter(Article.articleType_id == article_type_id).\
                update({Article.articleType_id: Constants.ARTICLE_TYPE_DEFAULT_ID})
            if auto_commit:
                db_session.commit()
        except Exception, e:
            logger.exception(e)


================================================
FILE: service/article_type_service.py
================================================
# coding=utf-8
import logging
from sqlalchemy.orm import contains_eager, joinedload
from model.models import ArticleType, ArticleTypeSetting
from model.search_params.article_type_params import ArticleTypeSearchParams
from . import BaseService
from article_service import ArticleService

logger = logging.getLogger(__name__)


class ArticleTypeService(object):
    @staticmethod
    def page_article_types(db_session, pager, search_params):
        query = db_session.query(ArticleType)
        if search_params:
            if search_params.order_mode == ArticleTypeSearchParams.ORDER_MODE_ID_DESC:
                query = query.order_by(ArticleType.id.desc())
            if search_params.show_setting:
                query = query.options(joinedload(ArticleType.setting))
        pager = BaseService.query_pager(query, pager)
        if pager.result:
            if search_params.show_articles_count:
                for article_type in pager.result:
                    article_type.fetch_articles_count()
        return pager

    @staticmethod
    def list_article_types_not_under_menu(db_session):
        article_types_not_under_menu = db_session.query(ArticleType).join(ArticleType.setting).\
            filter(ArticleType.menu_id.is_(None), ArticleTypeSetting.hide.isnot(True)).\
            options(contains_eager(ArticleType.setting)).all()
        return article_types_not_under_menu

    @staticmethod
    def add_article_type(db_session, article_type):
        try:
            article_type_to_add = ArticleType(name=article_type["name"], introduction=article_type["introduction"],
                                              menu_id=article_type["menu_id"],
                                              setting=ArticleTypeSetting(name=article_type["name"],
                                                                         hide=article_type["setting_hide"],),)
            db_session.add(article_type_to_add)
            db_session.commit()
            return article_type_to_add
        except Exception, e:
            logger.exception(e)
        return None

    @staticmethod
    def update_article_type(db_session, article_type_id, article_type):
        try:
            article_type_to_update=db_session.query(ArticleType).get(article_type_id)
            if article_type_to_update and not article_type_to_update.is_protected:
                article_type_to_update.name=article_type['name']
                article_type_to_update.introduction = article_type['introduction']
                article_type_to_update.menu_id = article_type['menu_id']
                if not article_type_to_update.setting:
                    article_type_to_update.setting = ArticleTypeSetting(name=article_type["name"],
                                                                        hide=article_type["setting_hide"],)
                else:
                    article_type_to_update.setting.hide = article_type['setting_hide']
                db_session.commit()
                return True
        except Exception, e:
            logger.exception(e)
        return False

    @staticmethod
    def delete(db_session, article_type_id):
        article_type_to_delete = db_session.query(ArticleType).get(article_type_id)
        if article_type_to_delete and not article_type_to_delete.is_protected:
            # 未将文章分类移除到未分类
            ArticleService.set_article_type_default_by_article_type_id(db_session, article_type_id, False)
            db_session.delete(article_type_to_delete.setting)
            db_session.delete(article_type_to_delete)
            db_session.commit()
            return 1
        return 0

    @staticmethod
    def set_article_type_menu_id_none(db_session, menu_id, auto_commit=True):
        db_session.query(ArticleType).filter(ArticleType.menu_id == menu_id).update({"menu_id": None})
        if auto_commit:
            db_session.commit()

    @staticmethod
    def list_simple(db_session):
        article_types = db_session.query(ArticleType.id, ArticleType.name).all()
        return article_types


================================================
FILE: service/blog_view_service.py
================================================
# coding=utf-8
import logging
import datetime
from model.models import BlogView

logger = logging.getLogger(__name__)


class BlogViewService(object):
    @staticmethod
    def get_blog_view(db_session, date=None):
        if not date:
            date = datetime.date.today()
        blog_view = db_session.query(BlogView).get(date)
        return blog_view

    @staticmethod
    def add_blog_view(db_session, add_pv, add_uv, date=None):
        if not date:
            date = datetime.date.today()
        blog_view = BlogViewService.get_blog_view(db_session, date)
        if blog_view:
            blog_view.pv = BlogView.pv + add_pv
            blog_view.uv = BlogView.uv + add_uv
        else:
            blog_view = BlogView(date=date, pv=add_pv, uv=add_uv)
            db_session.add(blog_view)
        db_session.commit()
        return blog_view


================================================
FILE: service/comment_service.py
================================================
# coding=utf-8
import logging

from model.models import Comment
from sqlalchemy.sql import func
from sqlalchemy.orm import joinedload
from model.search_params.comment_params import CommentSearchParams
from . import BaseService

logger = logging.getLogger(__name__)


class CommentService(object):
    @staticmethod
    def get_comment(db_session, comment_id):
        return db_session.query(Comment).get(comment_id)

    @staticmethod
    def get_max_floor(db_session, article_id):
        max_floor = db_session.query(func.max(Comment.floor)).filter(Comment.article_id == article_id).scalar()
        return max_floor if max_floor else 0;

    @staticmethod
    def add_comment(db_session, article_id, comment):
        max_floor = CommentService.get_max_floor(db_session, article_id)
        floor = max_floor + 1
        comment_to_add = Comment(content=comment['content'], author_name=comment['author_name'],
                                 author_email=comment['author_email'], article_id=article_id,
                                 comment_type=comment['comment_type'], rank=comment['rank'], floor=floor,
                                 reply_to_id=comment['reply_to_id'], reply_to_floor=comment['reply_to_floor'])
        db_session.add(comment_to_add)
        db_session.commit()
        return comment_to_add

    @staticmethod
    def update_comment_disabled(db_session, article_id, comment_id, disabled):
        updated = db_session.query(Comment).filter(Comment.article_id == article_id, Comment.id == comment_id).\
            update({Comment.disabled: disabled})
        db_session.commit()
        return updated

    @staticmethod
    def delete_comment(db_session, article_id, comment_id):
        comment = CommentService.get_comment(db_session, comment_id);
        if comment and comment.article_id == int(article_id):
            db_session.delete(comment)
            db_session.commit()
            return comment
        return None

    @staticmethod
    def page_comments(db_session, pager, params):
        query = db_session.query(Comment)
        if params:
            if params.article_id:
                query = query.filter(Comment.article_id == params.article_id)
            if params.show_article_id_title:
                query = query.options(joinedload(Comment.article).load_only("id", "title"))
            if params.order_mode == CommentSearchParams.ORDER_MODE_CREATE_TIME_ASC:
                query = query.order_by(Comment.create_time.asc())
            elif params.order_mode == CommentSearchParams.ORDER_MODE_CREATE_TIME_DESC:
                query = query.order_by(Comment.create_time.desc())
        pager = BaseService.query_pager(query, pager)
        return pager

    @staticmethod
    def remove_by_article_id(db_session, article_id, commit=True):
        try:
            comments = db_session.query(Comment).filter(Comment.article_id == article_id).all()
            db_session.query(Comment).filter(Comment.article_id == article_id).delete()
            if commit:
                db_session.commit()
            return comments
        except Exception, e:
            logger.exception(e)
        return None

    @staticmethod
    def get_comment_count(db_session):
        comment_count = db_session.query(Comment).count()
        return comment_count

    @staticmethod
    def get_comments_count_subquery(db_session):
        stmt = db_session.query(Comment.article_id, func.count('*').label('comments_count')). \
            group_by(Comment.article_id).subquery()
        return stmt

================================================
FILE: service/custom_service.py
================================================
# coding=utf-8
from model.models import BlogInfo

"""
博客定制相关服务
"""


class BlogInfoService(object):

    @staticmethod
    def get_blog_info(db_session):
        blog = db_session.query(BlogInfo).first()
        return blog

    @staticmethod
    def update_blog_info(db_session, blog_info):
        blog_info_old = BlogInfoService.get_blog_info(db_session)
        if blog_info_old is not None:
            if "title" in blog_info and blog_info['title'] is not None:
                blog_info_old.title = blog_info['title']
            if "signature" in blog_info and blog_info['signature'] is not None:
                blog_info_old.signature = blog_info['signature']
            if "navbar" in blog_info and blog_info['navbar'] is not None:
                blog_info_old.navbar = blog_info['navbar']
            db_session.commit()
        return blog_info_old


================================================
FILE: service/init_service.py
================================================
# coding=utf-8
import json
import logging

import tornado.gen

from article_service import ArticleService
from article_type_service import ArticleTypeService
from config import site_cache_keys
from custom_service import BlogInfoService
from extends.utils import AlchemyEncoder, Dict
from menu_service import MenuService
from model.site_info import SiteCollection
from model.constants import Constants
from plugin_service import PluginService
from comment_service import CommentService
from blog_view_service import BlogViewService
from config import config

logger = logging.getLogger(__name__)

"""
初始化相关,包括缓存管理
"""


class SiteCacheService(object):
    """SiteCache缓存策略
    站点缓存,加快访问速度,尤其是首页显示的相关数据,该类字段做二级缓存,本地缓存-redis缓存
    查询策略:先查本地缓存,未命中查询redis缓存,还未命中查询数据库,并将结果逐级更新
    更新策略:数据写入数据库后,更新redis缓存,并通过发布对应字段的更新消息通知所有节点更新本地缓存
    缓存校准:mater节点,设置定时任务,在访问较少的时间段校准redis缓存,并通知所有节点更新
    """
    PUB_SUB_MSGS = dict(
        blog_info_updated="blog_info_updated",  # blog_info更新消息
        plugins_updated="plugins_updated",  # plugins更新消息
        menus_updated="menus_updated",  # menus更新消息(包括query_article_types_not_under_menu)
        article_count_updated="article_count_updated",  # article_count更新消息
        article_sources_updated="article_sources_updated",  # article_sources更新消息
        source_articles_count_updated="source_articles_count_updated",  # 某source下的source_articles_count更新消息
        comment_count_updated='comment_count_updated',  # comment_count更新消息
        blog_view_count_updated='blog_view_count_updated',  # blog_view_count更新消息
    )

    @staticmethod
    @tornado.gen.coroutine
    def query_all(cache_manager, thread_do, db, is_pub_all=False, pubsub_manager=None):
        yield SiteCacheService.query_blog_info(cache_manager, thread_do, db, is_pub_all, pubsub_manager)
        yield SiteCacheService.query_menus(cache_manager, thread_do, db, is_pub_all, pubsub_manager)
        yield SiteCacheService.query_plugins(cache_manager, thread_do, db, is_pub_all, pubsub_manager)
        yield SiteCacheService.query_blog_view_count(cache_manager, thread_do, db, is_pub_all, pubsub_manager)
        yield SiteCacheService.query_article_count(cache_manager, thread_do, db, is_pub_all, pubsub_manager)
        yield SiteCacheService.query_comment_count(cache_manager, thread_do, db, is_pub_all, pubsub_manager)
        yield SiteCacheService.query_article_sources(cache_manager, thread_do, db, is_pub_all, pubsub_manager)

    @staticmethod
    @tornado.gen.coroutine
    def query_blog_info(cache_manager, thread_do, db, is_pub_all=False, pubsub_manager=None):
        title = yield cache_manager.call("GET", site_cache_keys['title'])
        signature = yield cache_manager.call("GET", site_cache_keys['signature'])
        navbar = yield cache_manager.call("GET", site_cache_keys['navbar'])
        if title is None or signature is None or navbar is None:
            blog_info = yield thread_do(BlogInfoService.get_blog_info, db)
            yield SiteCacheService.update_blog_info(cache_manager, blog_info, is_pub_all, pubsub_manager)
        else:
            SiteCollection.title = title
            SiteCollection.signature = signature
            SiteCollection.navbar = navbar

    @staticmethod
    @tornado.gen.coroutine
    def query_menus(cache_manager, thread_do, db, is_pub_all=False, pubsub_manager=None):
        menus_json = yield cache_manager.call("GET", site_cache_keys['menus'])
        menus = json.loads(menus_json, object_hook=Dict) if menus_json else None
        ats_json = yield cache_manager.call("GET", site_cache_keys['article_types_not_under_menu'])
        ats = json.loads(ats_json, object_hook=Dict) if ats_json else None
        if menus is None or ats is None:
            menus = yield thread_do(MenuService.list_menus, db, show_types=True)
            ats = yield thread_do(ArticleTypeService.list_article_types_not_under_menu, db)
            yield SiteCacheService.update_menus(cache_manager, menus, ats, is_pub_all, pubsub_manager)
        else:
            SiteCollection.menus = menus
            SiteCollection.article_types_not_under_menu = ats

    @staticmethod
    @tornado.gen.coroutine
    def query_plugins(cache_manager, thread_do, db, is_pub_all=False, pubsub_manager=None):
        plugins_json = yield cache_manager.call("GET", site_cache_keys['plugins'])
        plugins = json.loads(plugins_json, object_hook=Dict) if plugins_json else None
        if plugins is None:
            plugins = yield thread_do(PluginService.list_plugins, db)
            yield SiteCacheService.update_plugins(cache_manager, plugins, is_pub_all, pubsub_manager)
        else:
            SiteCollection.plugins = plugins

    @staticmethod
    @tornado.gen.coroutine
    def query_article_count(cache_manager, thread_do, db, is_pub_all=False, pubsub_manager=None):
        article_count = yield cache_manager.call("GET", site_cache_keys['article_count'])
        if article_count is None:
            article_count = yield thread_do(ArticleService.get_count, db)
            if article_count is not None:
                yield SiteCacheService.update_article_count(cache_manager, article_count, is_pub_all, pubsub_manager)
        else:
            SiteCollection.article_count = int(article_count)

    @staticmethod
    @tornado.gen.coroutine
    def query_article_sources(cache_manager, thread_do, db, is_pub_all=False, pubsub_manager=None):
        article_sources_json = yield cache_manager.call("GET", site_cache_keys['article_sources'])
        article_sources = json.loads(article_sources_json, object_hook=Dict) if article_sources_json else None
        if article_sources is None:
            article_sources = yield thread_do(ArticleService.get_article_sources, db)
            if article_sources is not None:
                yield SiteCacheService.update_article_sources(
                    cache_manager, article_sources, is_pub_all, pubsub_manager)
        else:
            SiteCollection.article_sources = article_sources
            yield SiteCacheService.query_source_articles_count(cache_manager)

    # 仅从cache中查询source下的source_articles_count
    @staticmethod
    @tornado.gen.coroutine
    def query_source_articles_count(cache_manager, source_id=None):
        flush_all_source_count = False if source_id else True
        if SiteCollection.article_sources:
            for source in SiteCollection.article_sources:
                if not flush_all_source_count and source.id != int(source_id):
                    continue
                count = yield cache_manager.call("GET", site_cache_keys['source_articles_count'].format(source.id))
                source.articles_count = int(count) if count else None

    @staticmethod
    @tornado.gen.coroutine
    def query_comment_count(cache_manager, thread_do, db, is_pub_all=False, pubsub_manager=None):
        comment_count = yield cache_manager.call("GET", site_cache_keys['comment_count'])
        if comment_count is None:
            comment_count = yield thread_do(CommentService.get_comment_count, db)
            if comment_count is not None:
                yield SiteCacheService.update_comment_count(cache_manager, comment_count, is_pub_all, pubsub_manager)
        else:
            SiteCollection.comment_count = int(comment_count)

    @staticmethod
    @tornado.gen.coroutine
    def query_blog_view_count(cache_manager, thread_do, db, is_pub_all=False, pubsub_manager=None):
        pv = yield cache_manager.call("GET", site_cache_keys['pv'])
        uv = yield cache_manager.call("GET", site_cache_keys['uv'])
        if pv is None or uv is None:
            blog_view = yield thread_do(BlogViewService.get_blog_view, db)
            pv = blog_view.pv if blog_view else 0
            uv = blog_view.uv if blog_view else 0
            yield SiteCacheService.update_blog_view_count(cache_manager, pv, uv, is_pub_all, pubsub_manager)
        else:
            SiteCollection.pv = pv
            SiteCollection.uv = uv

# 下面是缓存更新

    @staticmethod
    @tornado.gen.coroutine
    def update_by_sub_msg(msgs, cache_manager, thread_do, db):
        if not msgs:
            pass
        msg = msgs[0]
        if msg == SiteCacheService.PUB_SUB_MSGS['blog_info_updated']:
            yield SiteCacheService.query_blog_info(cache_manager, thread_do, db)
        elif msg == SiteCacheService.PUB_SUB_MSGS['plugins_updated']:
            yield SiteCacheService.query_plugins(cache_manager, thread_do, db)
        elif msg == SiteCacheService.PUB_SUB_MSGS['menus_updated']:
            yield SiteCacheService.query_menus(cache_manager, thread_do, db)
        elif msg == SiteCacheService.PUB_SUB_MSGS['article_count_updated']:
            yield SiteCacheService.query_article_count(cache_manager, thread_do, db)
        elif msg == SiteCacheService.PUB_SUB_MSGS['article_sources_updated']:
            yield SiteCacheService.query_article_sources(cache_manager, thread_do, db)
        elif msg == SiteCacheService.PUB_SUB_MSGS['comment_count_updated']:
            yield SiteCacheService.query_comment_count(cache_manager, thread_do, db)
        elif msg == SiteCacheService.PUB_SUB_MSGS['blog_view_count_updated']:
            yield SiteCacheService.query_blog_view_count(cache_manager, thread_do, db)
        else:
            try:
                ms = json.loads(msg)
                if ms[0] == SiteCacheService.PUB_SUB_MSGS['source_articles_count_updated']:
                    yield SiteCacheService.query_source_articles_count(cache_manager, ms[1])
            except Exception, e:
                logger.exception(e)

    @staticmethod
    @tornado.gen.coroutine
    def update_blog_info(cache_manager, blog_info, is_pub_all=False, pubsub_manager=None):
        SiteCollection.title = blog_info.title
        SiteCollection.signature = blog_info.signature
        SiteCollection.navbar = blog_info.navbar
        yield cache_manager.call("SET", site_cache_keys['title'], blog_info.title)
        yield cache_manager.call("SET", site_cache_keys['signature'], blog_info.signature)
        yield cache_manager.call("SET", site_cache_keys['navbar'], blog_info.navbar)
        if is_pub_all:
            yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['blog_info_updated'])

    @staticmethod
    @tornado.gen.coroutine
    def update_plugins(cache_manager, plugins, is_pub_all=False, pubsub_manager=None):
        if plugins is not None:
            SiteCollection.plugins = plugins
            plugins_json = json.dumps(plugins, cls=AlchemyEncoder)
            yield cache_manager.call("SET", site_cache_keys['plugins'], plugins_json)
            if is_pub_all:
                yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['plugins_updated'])

    @staticmethod
    @tornado.gen.coroutine
    def update_menus(cache_manager, menus, article_types_not_under_menu, is_pub_all=False, pubsub_manager=None):
        if menus is not None:
            SiteCollection.menus = menus
            menus_json = json.dumps(menus, cls=AlchemyEncoder)
            yield cache_manager.call("SET", site_cache_keys['menus'], menus_json)
        if article_types_not_under_menu is not None:
            SiteCollection.article_types_not_under_menu = article_types_not_under_menu
            ats_json = json.dumps(article_types_not_under_menu, cls=AlchemyEncoder)
            yield cache_manager.call("SET", site_cache_keys['article_types_not_under_menu'], ats_json)
        if is_pub_all:
            yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['menus_updated'])

    @staticmethod
    @tornado.gen.coroutine
    def update_article_count(cache_manager, article_count, is_pub_all=False, pubsub_manager=None):
        if article_count is not None:
            SiteCollection.article_count = article_count
            yield cache_manager.call("SET", site_cache_keys['article_count'], article_count)
            if is_pub_all:
                yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['article_count_updated'])

    @staticmethod
    @tornado.gen.coroutine
    def update_comment_count(cache_manager, comment_count, is_pub_all=False, pubsub_manager=None):
        if comment_count is not None:
            SiteCollection.comment_count = comment_count
            yield cache_manager.call("SET", site_cache_keys['comment_count'], comment_count)
            if is_pub_all:
                yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['comment_count_updated'])

    @staticmethod
    @tornado.gen.coroutine
    def update_blog_view_count(cache_manager, pv, uv, is_pub_all=False, pubsub_manager=None):
        if pv is not None and uv is not None:
            SiteCollection.pv = pv
            SiteCollection.uv = uv
            yield cache_manager.call("SET", site_cache_keys['pv'], pv)
            yield cache_manager.call("SET", site_cache_keys['uv'], uv)
            if is_pub_all:
                yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['blog_view_count_updated'])

    @staticmethod
    @tornado.gen.coroutine
    def update_article_sources(cache_manager, article_sources, is_pub_all=False, pubsub_manager=None):
        if article_sources is not None:
            SiteCollection.article_sources = article_sources
            article_sources_json = json.dumps(article_sources, cls=AlchemyEncoder)
            yield cache_manager.call("SET", site_cache_keys['article_sources'], article_sources_json)
            #  记录对应source下的article_count
            for source in SiteCollection.article_sources:
                yield cache_manager.call("SET", site_cache_keys['source_articles_count'].format(source.id),
                                         source.articles_count)
            if is_pub_all:
                yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['article_sources_updated'])

    # article增删改后的操作article_count以及对应的source_count
    @staticmethod
    @tornado.gen.coroutine
    def update_article_action(cache_manager, action, article, is_pub_all=False, pubsub_manager=None):
        if action == Constants.FLUSH_ARTICLE_ACTION_ADD:
            article_count = yield cache_manager.call("INCR", site_cache_keys['article_count'])
            if article_count:
                SiteCollection.article_count = article_count
                if is_pub_all:
                    yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['article_count_updated'])
            #  注意: 上面的article_count在并发环境下是可以保证安全的,
            #  如果用GET SET会比较难实现。具体该并发问题可以参考:http://www.cnblogs.com/iforever/p/5796902.html
            article_source_id = int(article.source_id)
            source_article_count = \
                yield cache_manager.call("INCR", site_cache_keys['source_articles_count'].format(article_source_id))
            for article_source in SiteCollection.article_sources:
                if int(article_source.id) == article_source_id:
                    article_source.articles_count = source_article_count
                    break
            if is_pub_all:
                yield pubsub_manager.pub_call(json.dumps(
                        [SiteCacheService.PUB_SUB_MSGS['source_articles_count_updated'], article_source_id]))
        if action == Constants.FLUSH_ARTICLE_ACTION_REMOVE:
            article_count = yield cache_manager.call("DECR", site_cache_keys['article_count'])
            if article_count:
                SiteCollection.article_count = article_count
                if is_pub_all:
                    yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['article_count_updated'])
                #  注意: 上面的article_count在并发环境下是可以保证安全的,
                #  如果用GET SET会比较难实现。具体该并发问题可以参考:http://www.cnblogs.com/iforever/p/5796902.html
                article_source_id = int(article.source_id)
                source_article_count = \
                    yield cache_manager.call("DECR",
                                             site_cache_keys['source_articles_count'].format(article_source_id))
                for article_source in SiteCollection.article_sources:
                    if int(article_source.id) == article_source_id:
                        article_source.articles_count = source_article_count
                        break
                if is_pub_all:
                    yield pubsub_manager.pub_call(json.dumps(
                        [SiteCacheService.PUB_SUB_MSGS['source_articles_count_updated'], article_source_id]))
        if action == Constants.FLUSH_ARTICLE_ACTION_UPDATE:
            article_new = article[0]
            article_old = article[1]
            source_id_old = int(article_old.source_id)
            source_id_new = int(article_new.source_id)
            if source_id_old != source_id_new:
                source_old_article_count = \
                    yield cache_manager.call("DECR",site_cache_keys['source_articles_count'].format(source_id_old))
                source_new_article_count = \
                    yield cache_manager.call("INCR",site_cache_keys['source_articles_count'].format(source_id_new))
                for article_source in SiteCollection.article_sources:
                    if int(article_source.id) == source_id_old:
                        article_source.articles_count = source_old_article_count
                    if int(article_source.id) == source_id_new:
                        article_source.articles_count = source_new_article_count
                if is_pub_all:
                    yield pubsub_manager.pub_call(json.dumps(
                        [SiteCacheService.PUB_SUB_MSGS['source_articles_count_updated'], source_id_old]))
                    yield pubsub_manager.pub_call(json.dumps(
                        [SiteCacheService.PUB_SUB_MSGS['source_articles_count_updated'], source_id_new]))

    @staticmethod
    @tornado.gen.coroutine
    def update_comment_action(cache_manager, action, comments, is_pub_all=False, pubsub_manager=None):
        if comments:
            comment_count = 1
            if isinstance(comments, list):
                comment_count = len(comments)
            if action == Constants.FLUSH_COMMENT_ACTION_ADD:
                SiteCollection.comment_count = yield cache_manager.\
                    call("INCRBY", site_cache_keys['comment_count'], comment_count)
                if is_pub_all:
                    yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['comment_count_updated'])
            elif action == Constants.FLUSH_COMMENT_ACTION_REMOVE:
                SiteCollection.comment_count = yield cache_manager.\
                    call("DECRBY", site_cache_keys['comment_count'], comment_count)
                if is_pub_all:
                    yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['comment_count_updated'])

    @staticmethod
    @tornado.gen.coroutine
    def add_pv_uv(cache_manager, add_pv, add_uv, is_pub_all=False, pubsub_manager=None):
        if add_pv or add_uv:
            if add_pv:
                SiteCollection.pv = yield cache_manager.\
                    call("INCRBY", site_cache_keys['pv'], add_pv)
            if add_uv:
                SiteCollection.uv = yield cache_manager. \
                    call("INCRBY", site_cache_keys['uv'], add_uv)
            if is_pub_all:
                yield pubsub_manager.pub_call(SiteCacheService.PUB_SUB_MSGS['blog_view_count_updated'])


"""
刷新所有缓存,从数据库重建缓存并通知其他节点,用于定时任务校准缓存
"""
@tornado.gen.coroutine
def flush_all_cache():
    application = config['application']
    thread_do = application.thread_executor.submit
    db = application.db_pool()
    cache_manager = application.cache_manager
    pubsub_manager = application.pubsub_manager
    yield cache_manager.call("DEL", *get_all_site_cache_keys())
    yield SiteCacheService.query_all(cache_manager, thread_do, db, True, pubsub_manager)


def get_all_site_cache_keys():
    keys = site_cache_keys.values()
    keys.remove(site_cache_keys['source_articles_count'])
    if SiteCollection.article_sources:
        for source in SiteCollection.article_sources:
            keys.append(site_cache_keys['source_articles_count'].format(source.id))
    return keys


================================================
FILE: service/menu_service.py
================================================
# coding=utf-8
import logging

from sqlalchemy import func

from article_type_service import ArticleTypeService
from model.models import Menu
from model.search_params.menu_params import MenuSearchParams
from . import BaseService

logger = logging.getLogger(__name__)


class MenuService(object):
    @staticmethod
    def page_menus(db_session, pager, search_params):
        query = db_session.query(Menu)
        if search_params:
            if search_params.order_mode == MenuSearchParams.ORDER_MODE_ORDER_ASC:
                query = query.order_by(Menu.order.asc())
        pager = BaseService.query_pager(query, pager)
        if pager.result:
            for menu in pager.result:
                menu.fetch_all_types()
        return pager

    @staticmethod
    def add_menu(db_session, menu):
        try:
            menu_to_save = Menu(**menu)
            menu_to_save.order = MenuService.get_max_order(db_session) + 1
            db_session.add(menu_to_save)
            db_session.commit()
            return menu_to_save
        except Exception, e:
            logger.exception(e)
        return None

    @staticmethod
    def get_max_order(db_session):
        max_order = db_session.query(func.max(Menu.order)).scalar()
        if max_order is None:
            max_order = 0
        return max_order

    @staticmethod
    def list_menus(db_session, show_types=False):
        menus = db_session.query(Menu).order_by(Menu.order.asc()).all()
        if not menus:
            menus = []
        else:
            if show_types:
                for menu in menus:
                    menu.fetch_all_types(only_show_not_hide=True)
        return menus

    @staticmethod
    def sort_up(db_session, menu_id):
        menu = db_session.query(Menu).get(menu_id)
        if menu:
            menu_up = db_session.query(Menu). \
                filter(Menu.order < menu.order).order_by(Menu.order.desc()).first()
            if menu_up:
                order_tmp = menu.order
                menu.order = menu_up.order
                menu_up.order = order_tmp
                db_session.commit()
                return True
        return False

    @staticmethod
    def sort_down(db_session, menu_id):
        menu = db_session.query(Menu).get(menu_id)
        if menu:
            menu_up = db_session.query(Menu). \
                filter(Menu.order > menu.order).order_by(Menu.order.asc()).first()
            if menu_up:
                order_tmp = menu.order
                menu.order = menu_up.order
                menu_up.order = order_tmp
                db_session.commit()
                return True
        return False

    @staticmethod
    def update(db_session, menu_id, menu_to_update):
        count = 0
        if menu_to_update:
            if "id" in menu_to_update:
                menu_to_update.remove("id")
            count = db_session.query(Menu).filter(Menu.id == menu_id).update(menu_to_update)
            if count:
                db_session.commit()
        return count

    @staticmethod
    def delete(db_session, menu_id):
        ArticleTypeService.set_article_type_menu_id_none(db_session, menu_id, auto_commit=False)
        count = db_session.query(Menu).filter(Menu.id == menu_id).delete()
        if count:
            db_session.commit()
        return count


================================================
FILE: service/plugin_service.py
================================================
# coding=utf-8
import logging
from sqlalchemy import func
from model.models import Plugin
from model.search_params.plugin_params import PluginSearchParams
from . import BaseService

logger = logging.getLogger(__name__)


class PluginService(object):

    @staticmethod
    def get(db_session, plugin_id):
        plugin = db_session.query(Plugin).get(plugin_id)
        return plugin

    @staticmethod
    def get_editable(db_session, plugin_id):
        plugin = db_session.query(Plugin).get(plugin_id)
        if plugin:
            plugin = plugin if plugin.content != 'system_plugin' else None
        return plugin

    @staticmethod
    def list_plugins(db_session):
        plugins = db_session.query(Plugin).order_by(Plugin.order.asc()).all()
        return plugins

    @staticmethod
    def page_plugins(db_session, pager, search_params):
        query = db_session.query(Plugin)
        if search_params:
            if search_params.order_mode == PluginSearchParams.ORDER_MODE_ORDER_ASC:
                query = query.order_by(Plugin.order.asc())
        pager = BaseService.query_pager(query, pager)
        return pager

    @staticmethod
    def save(db_session, plugin):
        try:
            plugin_to_save = Plugin(**plugin)
            plugin_to_save.order = PluginService.get_max_order(db_session) + 1
            db_session.add(plugin_to_save)
            db_session.commit()
            return plugin_to_save
        except Exception, e:
            logger.exception(e)
        return None

    @staticmethod
    def get_max_order(db_session):
        max_order = db_session.query(func.max(Plugin.order)).scalar()
        if max_order is None:
            max_order = 0
        return max_order

    @staticmethod
    def sort_up(db_session, plugin_id):
        plugin = db_session.query(Plugin).get(plugin_id)
        if plugin:
            plugin_up = db_session.query(Plugin).\
                filter(Plugin.order < plugin.order).order_by(Plugin.order.desc()).first()
            if plugin_up:
                order_tmp = plugin.order
                plugin.order = plugin_up.order
                plugin_up.order = order_tmp
                db_session.commit()
                return True
        return False

    @staticmethod
    def sort_down(db_session, plugin_id):
        plugin = db_session.query(Plugin).get(plugin_id)
        if plugin:
            plugin_up = db_session.query(Plugin).\
                filter(Plugin.order > plugin.order).order_by(Plugin.order.asc()).first()
            if plugin_up:
                order_tmp = plugin.order
                plugin.order = plugin_up.order
                plugin_up.order = order_tmp
                db_session.commit()
                return True
        return False

    @staticmethod
    def update_disabled(db_session, plugin_id, disabled):
        update_count = db_session.query(Plugin).filter(Plugin.id == plugin_id).update({Plugin.disabled:disabled})
        if update_count:
            db_session.commit()
        return update_count

    @staticmethod
    def delete(db_session, plugin_id):
        plugin = PluginService.get_editable(db_session, plugin_id)
        if plugin:
            db_session.delete(plugin)
            db_session.commit()
            return True
        return False

    @staticmethod
    def update(db_session, plugin_id, plugin_to_update):
        plugin = PluginService.get_editable(db_session, plugin_id)
        if plugin:
            plugin.title = plugin_to_update['title']
            plugin.note = plugin_to_update['note']
            plugin.content = plugin_to_update['content']
            db_session.commit()
            return True
        return False



================================================
FILE: service/pubsub_service.py
================================================
# coding=utf-8
import logging
import tornado.gen
from extends.pub_sub_tornadis import PubSubTornadis
from init_service import SiteCacheService
from config import redis_pub_sub_channels

logger = logging.getLogger(__name__)


class PubSubService(PubSubTornadis):

    def __init__(self, redis_pub_sub_config, application, loop=None):
        super(PubSubService, self).__init__(redis_pub_sub_config, loop)
        self.application = application
        self.db_pool = self.application.db_pool
        self.cache_manager = self.application.cache_manager
        self.thread_executor = self.application.thread_executor
        self.thread_do = self.thread_executor.submit
        self._db_session = None

    @property
    def db(self):
        if not self._db_session:
            self._db_session = self.application.db_pool()
        return self._db_session

    @tornado.gen.coroutine
    def first_do_after_subscribed(self):
        yield SiteCacheService.query_all(self.cache_manager, self.thread_do, self.db)

    @tornado.gen.coroutine
    def do_msg(self, msgs):
        logger.info("收到redis消息: " + str(msgs))
        if len(msgs) >= 3:
            channel = msgs[1]
            core_msgs = msgs[2:]
            if channel == redis_pub_sub_channels['cache_message_channel']:
                yield SiteCacheService.update_by_sub_msg(core_msgs, self.cache_manager, self.thread_do, self.db)

================================================
FILE: service/user_service.py
================================================
# coding=utf-8
from model.models import User


class UserService(object):

    @staticmethod
    def get_user(db_session, username):
        return db_session.query(User).filter(User.username == username).first()

    @staticmethod
    def update_user_info(db_session, username, password, user):
        current_user = UserService.get_user(db_session, username)
        if current_user and current_user.password == password:
            if "username" in user:
                current_user.username = user['username']
            if "email" in user:
                current_user.email = user['email']
            db_session.commit()
            return current_user
        else:
            return None

    @staticmethod
    def update_password(db_session, username, old_password, new_password):
        count = db_session.query(User).filter(User.username == username, User.password == old_password)\
            .update({"password":new_password})
        db_session.commit()
        return count

    @staticmethod
    def get_count(db_session):
        return db_session.query(User).count()

    @staticmethod
    def save_user(db_session, user):
        user_to_save = User(
            email=user['email'],
            username=user['username'],
            password=user['password'],
        )
        db_session.add(user_to_save)
        db_session.commit()
        return user_to_save

================================================
FILE: static/css/bootstrap-theme.css
================================================
/*!
 * Bootstrap v3.3.5 (http://getbootstrap.com)
 * Copyright 2011-2015 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 */
.btn-default,
.btn-primary,
.btn-success,
.btn-info,
.btn-warning,
.btn-danger {
  text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
}
.btn-default:active,
.btn-primary:active,
.btn-success:active,
.btn-info:active,
.btn-warning:active,
.btn-danger:active,
.btn-default.active,
.btn-primary.active,
.btn-success.active,
.btn-info.active,
.btn-warning.active,
.btn-danger.active {
  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn-default.disabled,
.btn-primary.disabled,
.btn-success.disabled,
.btn-info.disabled,
.btn-warning.disabled,
.btn-danger.disabled,
.btn-default[disabled],
.btn-primary[disabled],
.btn-success[disabled],
.btn-info[disabled],
.btn-warning[disabled],
.btn-danger[disabled],
fieldset[disabled] .btn-default,
fieldset[disabled] .btn-primary,
fieldset[disabled] .btn-success,
fieldset[disabled] .btn-info,
fieldset[disabled] .btn-warning,
fieldset[disabled] .btn-danger {
  -webkit-box-shadow: none;
          box-shadow: none;
}
.btn-default .badge,
.btn-primary .badge,
.btn-success .badge,
.btn-info .badge,
.btn-warning .badge,
.btn-danger .badge {
  text-shadow: none;
}
.btn:active,
.btn.active {
  background-image: none;
}
.btn-default {
  text-shadow: 0 1px 0 #fff;
  background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
  background-image:      -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
  background-image:         linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
  background-repeat: repeat-x;
  border-color: #dbdbdb;
  border-color: #ccc;
}
.btn-default:hover,
.btn-default:focus {
  background-color: #e0e0e0;
  background-position: 0 -15px;
}
.btn-default:active,
.btn-default.active {
  background-color: #e0e0e0;
  border-color: #dbdbdb;
}
.btn-default.disabled,
.btn-default[disabled],
fieldset[disabled] .btn-default,
.btn-default.disabled:hover,
.btn-default[disabled]:hover,
fieldset[disabled] .btn-default:hover,
.btn-default.disabled:focus,
.btn-default[disabled]:focus,
fieldset[disabled] .btn-default:focus,
.btn-default.disabled.focus,
.btn-default[disabled].focus,
fieldset[disabled] .btn-default.focus,
.btn-default.disabled:active,
.btn-default[disabled]:active,
fieldset[disabled] .btn-default:active,
.btn-default.disabled.active,
.btn-default[disabled].active,
fieldset[disabled] .btn-default.active {
  background-color: #e0e0e0;
  background-image: none;
}
.btn-primary {
  background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
  background-image:      -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
  background-image:         linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
  background-repeat: repeat-x;
  border-color: #245580;
}
.btn-primary:hover,
.btn-primary:focus {
  background-color: #265a88;
  background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
  background-color: #265a88;
  border-color: #245580;
}
.btn-primary.disabled,
.btn-primary[disabled],
fieldset[disabled] .btn-primary,
.btn-primary.disabled:hover,
.btn-primary[disabled]:hover,
fieldset[disabled] .btn-primary:hover,
.btn-primary.disabled:focus,
.btn-primary[disabled]:focus,
fieldset[disabled] .btn-primary:focus,
.btn-primary.disabled.focus,
.btn-primary[disabled].focus,
fieldset[disabled] .btn-primary.focus,
.btn-primary.disabled:active,
.btn-primary[disabled]:active,
fieldset[disabled] .btn-primary:active,
.btn-primary.disabled.active,
.btn-primary[disabled].active,
fieldset[disabled] .btn-primary.active {
  background-color: #265a88;
  background-image: none;
}
.btn-success {
  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
  background-image:      -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
  background-image:         linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
  background-repeat: repeat-x;
  border-color: #3e8f3e;
}
.btn-success:hover,
.btn-success:focus {
  background-color: #419641;
  background-position: 0 -15px;
}
.btn-success:active,
.btn-success.active {
  background-color: #419641;
  border-color: #3e8f3e;
}
.btn-success.disabled,
.btn-success[disabled],
fieldset[disabled] .btn-success,
.btn-success.disabled:hover,
.btn-success[disabled]:hover,
fieldset[disabled] .btn-success:hover,
.btn-success.disabled:focus,
.btn-success[disabled]:focus,
fieldset[disabled] .btn-success:focus,
.btn-success.disabled.focus,
.btn-success[disabled].focus,
fieldset[disabled] .btn-success.focus,
.btn-success.disabled:active,
.btn-success[disabled]:active,
fieldset[disabled] .btn-success:active,
.btn-success.disabled.active,
.btn-success[disabled].active,
fieldset[disabled] .btn-success.active {
  background-color: #419641;
  background-image: none;
}
.btn-info {
  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
  background-image:      -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
  background-image:         linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
  background-repeat: repeat-x;
  border-color: #28a4c9;
}
.btn-info:hover,
.btn-info:focus {
  background-color: #2aabd2;
  background-position: 0 -15px;
}
.btn-info:active,
.btn-info.active {
  background-color: #2aabd2;
  border-color: #28a4c9;
}
.btn-info.disabled,
.btn-info[disabled],
fieldset[disabled] .btn-info,
.btn-info.disabled:hover,
.btn-info[disabled]:hover,
fieldset[disabled] .btn-info:hover,
.btn-info.disabled:focus,
.btn-info[disabled]:focus,
fieldset[disabled] .btn-info:focus,
.btn-info.disabled.focus,
.btn-info[disabled].focus,
fieldset[disabled] .btn-info.focus,
.btn-info.disabled:active,
.btn-info[disabled]:active,
fieldset[disabled] .btn-info:active,
.btn-info.disabled.active,
.btn-info[disabled].active,
fieldset[disabled] .btn-info.active {
  background-color: #2aabd2;
  background-image: none;
}
.btn-warning {
  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
  background-repeat: repeat-x;
  border-color: #e38d13;
}
.btn-warning:hover,
.btn-warning:focus {
  background-color: #eb9316;
  background-position: 0 -15px;
}
.btn-warning:active,
.btn-warning.active {
  background-color: #eb9316;
  border-color: #e38d13;
}
.btn-warning.disabled,
.btn-warning[disabled],
fieldset[disabled] .btn-warning,
.btn-warning.disabled:hover,
.btn-warning[disabled]:hover,
fieldset[disabled] .btn-warning:hover,
.btn-warning.disabled:focus,
.btn-warning[disabled]:focus,
fieldset[disabled] .btn-warning:focus,
.btn-warning.disabled.focus,
.btn-warning[disabled].focus,
fieldset[disabled] .btn-warning.focus,
.btn-warning.disabled:active,
.btn-warning[disabled]:active,
fieldset[disabled] .btn-warning:active,
.btn-warning.disabled.active,
.btn-warning[disabled].active,
fieldset[disabled] .btn-warning.active {
  background-color: #eb9316;
  background-image: none;
}
.btn-danger {
  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
  background-image:      -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
  background-image:         linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
  background-repeat: repeat-x;
  border-color: #b92c28;
}
.btn-danger:hover,
.btn-danger:focus {
  background-color: #c12e2a;
  background-position: 0 -15px;
}
.btn-danger:active,
.btn-danger.active {
  background-color: #c12e2a;
  border-color: #b92c28;
}
.btn-danger.disabled,
.btn-danger[disabled],
fieldset[disabled] .btn-danger,
.btn-danger.disabled:hover,
.btn-danger[disabled]:hover,
fieldset[disabled] .btn-danger:hover,
.btn-danger.disabled:focus,
.btn-danger[disabled]:focus,
fieldset[disabled] .btn-danger:focus,
.btn-danger.disabled.focus,
.btn-danger[disabled].focus,
fieldset[disabled] .btn-danger.focus,
.btn-danger.disabled:active,
.btn-danger[disabled]:active,
fieldset[disabled] .btn-danger:active,
.btn-danger.disabled.active,
.btn-danger[disabled].active,
fieldset[disabled] .btn-danger.active {
  background-color: #c12e2a;
  background-image: none;
}
.thumbnail,
.img-thumbnail {
  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
  background-color: #e8e8e8;
  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
  background-repeat: repeat-x;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
  background-color: #2e6da4;
  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
  background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
  background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
  background-repeat: repeat-x;
}
.navbar-default {
  background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
  background-image:      -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
  background-image:         linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
  background-repeat: repeat-x;
  border-radius: 4px;
  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
}
.navbar-default .navbar-nav > .open > a,
.navbar-default .navbar-nav > .active > a {
  background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
  background-image:      -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
  background-image:         linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
  background-repeat: repeat-x;
  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
}
.navbar-brand,
.navbar-nav > li > a {
  text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
}
.navbar-inverse {
  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
  background-image:      -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
  background-image:         linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
  background-repeat: repeat-x;
  border-radius: 4px;
}
.navbar-inverse .navbar-nav > .open > a,
.navbar-inverse .navbar-nav > .active > a {
  background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
  background-image:      -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
  background-image:         linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
  background-repeat: repeat-x;
  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-nav > li > a {
  text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
}
.navbar-static-top,
.navbar-fixed-top,
.navbar-fixed-bottom {
  border-radius: 0;
}
@media (max-width: 767px) {
  .navbar .navbar-nav .open .dropdown-menu > .active > a,
  .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
  .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
    color: #fff;
    background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
    background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
    background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
    background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
    background-repeat: repeat-x;
  }
}
.alert {
  text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
}
.alert-success {
  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
  background-image:      -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
  background-image:         linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
  background-repeat: repeat-x;
  border-color: #b2dba1;
}
.alert-info {
  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
  background-image:      -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
  background-image:         linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
  background-repeat: repeat-x;
  border-color: #9acfea;
}
.alert-warning {
  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
  background-repeat: repeat-x;
  border-color: #f5e79e;
}
.alert-danger {
  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
  background-image:      -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
  background-image:         linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
  background-repeat: repeat-x;
  border-color: #dca7a7;
}
.progress {
  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
  background-image:      -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
  background-repeat: repeat-x;
}
.progress-bar {
  background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);
  background-image:      -o-linear-gradient(top, #337ab7 0%, #286090 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));
  background-image:         linear-gradient(to bottom, #337ab7 0%, #286090 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);
  background-repeat: repeat-x;
}
.progress-bar-success {
  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
  background-image:      -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
  background-image:         linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
  background-repeat: repeat-x;
}
.progress-bar-info {
  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
  background-image:      -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
  background-image:         linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
  background-repeat: repeat-x;
}
.progress-bar-warning {
  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
  background-repeat: repeat-x;
}
.progress-bar-danger {
  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
  background-image:      -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
  background-image:         linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
  background-repeat: repeat-x;
}
.progress-bar-striped {
  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
}
.list-group {
  border-radius: 4px;
  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.list-group-item.active,
.list-group-item.active:hover,
.list-group-item.active:focus {
  text-shadow: 0 -1px 0 #286090;
  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);
  background-image:      -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));
  background-image:         linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);
  background-repeat: repeat-x;
  border-color: #2b669a;
}
.list-group-item.active .badge,
.list-group-item.active:hover .badge,
.list-group-item.active:focus .badge {
  text-shadow: none;
}
.panel {
  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
          box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
}
.panel-default > .panel-heading {
  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
  background-repeat: repeat-x;
}
.panel-primary > .panel-heading {
  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
  background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
  background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
  background-repeat: repeat-x;
}
.panel-success > .panel-heading {
  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
  background-image:      -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
  background-image:         linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
  background-repeat: repeat-x;
}
.panel-info > .panel-heading {
  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
  background-image:      -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
  background-image:         linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
  background-repeat: repeat-x;
}
.panel-warning > .panel-heading {
  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
  background-repeat: repeat-x;
}
.panel-danger > .panel-heading {
  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
  background-image:      -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
  background-image:         linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
  background-repeat: repeat-x;
}
.well {
  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
  background-image:      -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
  background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
  background-image:         linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
  background-repeat: repeat-x;
  border-color: #dcdcdc;
  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
          box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
}
/*# sourceMappingURL=bootstrap-theme.css.map */


================================================
FILE: static/css/bootstrap.css
================================================
/*!
 * Bootstrap v3.3.5 (http://getbootstrap.com)
 * Copyright 2011-2015 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 */
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
html {
  font-family: sans-serif;
  -webkit-text-size-adjust: 100%;
      -ms-text-size-adjust: 100%;
}
body {
  margin: 0;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
  display: block;
}
audio,
canvas,
progress,
video {
  display: inline-block;
  vertical-align: baseline;
}
audio:not([controls]) {
  display: none;
  height: 0;
}
[hidden],
template {
  display: none;
}
a {
  background-color: transparent;
}
a:active,
a:hover {
  outline: 0;
}
abbr[title] {
  border-bottom: 1px dotted;
}
b,
strong {
  font-weight: bold;
}
dfn {
  font-style: italic;
}
h1 {
  margin: .67em 0;
  font-size: 2em;
}
mark {
  color: #000;
  background: #ff0;
}
small {
  font-size: 80%;
}
sub,
sup {
  position: relative;
  font-size: 75%;
  line-height: 0;
  vertical-align: baseline;
}
sup {
  top: -.5em;
}
sub {
  bottom: -.25em;
}
img {
  border: 0;
}
svg:not(:root) {
  overflow: hidden;
}
figure {
  margin: 1em 40px;
}
hr {
  height: 0;
  -webkit-box-sizing: content-box;
     -moz-box-sizing: content-box;
          box-sizing: content-box;
}
pre {
  overflow: auto;
}
code,
kbd,
pre,
samp {
  font-family: monospace, monospace;
  font-size: 1em;
}
button,
input,
optgroup,
select,
textarea {
  margin: 0;
  font: inherit;
  color: inherit;
}
button {
  overflow: visible;
}
button,
select {
  text-transform: none;
}
button,
html input[type="button"],
input[type="reset"],
input[type="submit"] {
  -webkit-appearance: button;
  cursor: pointer;
}
button[disabled],
html input[disabled] {
  cursor: default;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
  padding: 0;
  border: 0;
}
input {
  line-height: normal;
}
input[type="checkbox"],
input[type="radio"] {
  -webkit-box-sizing: border-box;
     -moz-box-sizing: border-box;
          box-sizing: border-box;
  padding: 0;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  height: auto;
}
input[type="search"] {
  -webkit-box-sizing: content-box;
     -moz-box-sizing: content-box;
          box-sizing: content-box;
  -webkit-appearance: textfield;
}
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}
fieldset {
  padding: .35em .625em .75em;
  margin: 0 2px;
  border: 1px solid #c0c0c0;
}
legend {
  padding: 0;
  border: 0;
}
textarea {
  overflow: auto;
}
optgroup {
  font-weight: bold;
}
table {
  border-spacing: 0;
  border-collapse: collapse;
}
td,
th {
  padding: 0;
}
/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */
@media print {
  *,
  *:before,
  *:after {
    color: #000 !important;
    text-shadow: none !important;
    background: transparent !important;
    -webkit-box-shadow: none !important;
            box-shadow: none !important;
  }
  a,
  a:visited {
    text-decoration: underline;
  }
  a[href]:after {
    content: " (" attr(href) ")";
  }
  abbr[title]:after {
    content: " (" attr(title) ")";
  }
  a[href^="#"]:after,
  a[href^="javascript:"]:after {
    content: "";
  }
  pre,
  blockquote {
    border: 1px solid #999;

    page-break-inside: avoid;
  }
  thead {
    display: table-header-group;
  }
  tr,
  img {
    page-break-inside: avoid;
  }
  img {
    max-width: 100% !important;
  }
  p,
  h2,
  h3 {
    orphans: 3;
    widows: 3;
  }
  h2,
  h3 {
    page-break-after: avoid;
  }
  .navbar {
    display: none;
  }
  .btn > .caret,
  .dropup > .btn > .caret {
    border-top-color: #000 !important;
  }
  .label {
    border: 1px solid #000;
  }
  .table {
    border-collapse: collapse !important;
  }
  .table td,
  .table th {
    background-color: #fff !important;
  }
  .table-bordered th,
  .table-bordered td {
    border: 1px solid #ddd !important;
  }
}
@font-face {
  font-family: 'Glyphicons Halflings';

  src: url('../fonts/glyphicons-halflings-regular.eot');
  src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
}
.glyphicon {
  position: relative;
  top: 1px;
  display: inline-block;
  font-family: 'Glyphicons Halflings';
  font-style: normal;
  font-weight: normal;
  line-height: 1;

  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.glyphicon-asterisk:before {
  content: "\2a";
}
.glyphicon-plus:before {
  content: "\2b";
}
.glyphicon-euro:before,
.glyphicon-eur:before {
  content: "\20ac";
}
.glyphicon-minus:before {
  content: "\2212";
}
.glyphicon-cloud:before {
  content: "\2601";
}
.glyphicon-envelope:before {
  content: "\2709";
}
.glyphicon-pencil:before {
  content: "\270f";
}
.glyphicon-glass:before {
  content: "\e001";
}
.glyphicon-music:before {
  content: "\e002";
}
.glyphicon-search:before {
  content: "\e003";
}
.glyphicon-heart:before {
  content: "\e005";
}
.glyphicon-star:before {
  content: "\e006";
}
.glyphicon-star-empty:before {
  content: "\e007";
}
.glyphicon-user:before {
  content: "\e008";
}
.glyphicon-film:before {
  content: "\e009";
}
.glyphicon-th-large:before {
  content: "\e010";
}
.glyphicon-th:before {
  content: "\e011";
}
.glyphicon-th-list:before {
  content: "\e012";
}
.glyphicon-ok:before {
  content: "\e013";
}
.glyphicon-remove:before {
  content: "\e014";
}
.glyphicon-zoom-in:before {
  content: "\e015";
}
.glyphicon-zoom-out:before {
  content: "\e016";
}
.glyphicon-off:before {
  content: "\e017";
}
.glyphicon-signal:before {
  content: "\e018";
}
.glyphicon-cog:before {
  content: "\e019";
}
.glyphicon-trash:before {
  content: "\e020";
}
.glyphicon-home:before {
  content: "\e021";
}
.glyphicon-file:before {
  content: "\e022";
}
.glyphicon-time:before {
  content: "\e023";
}
.glyphicon-road:before {
  content: "\e024";
}
.glyphicon-download-alt:before {
  content: "\e025";
}
.glyphicon-download:before {
  content: "\e026";
}
.glyphicon-upload:before {
  content: "\e027";
}
.glyphicon-inbox:before {
  content: "\e028";
}
.glyphicon-play-circle:before {
  content: "\e029";
}
.glyphicon-repeat:before {
  content: "\e030";
}
.glyphicon-refresh:before {
  content: "\e031";
}
.glyphicon-list-alt:before {
  content: "\e032";
}
.glyphicon-lock:before {
  content: "\e033";
}
.glyphicon-flag:before {
  content: "\e034";
}
.glyphicon-headphones:before {
  content: "\e035";
}
.glyphicon-volume-off:before {
  content: "\e036";
}
.glyphicon-volume-down:before {
  content: "\e037";
}
.glyphicon-volume-up:before {
  content: "\e038";
}
.glyphicon-qrcode:before {
  content: "\e039";
}
.glyphicon-barcode:before {
  content: "\e040";
}
.glyphicon-tag:before {
  content: "\e041";
}
.glyphicon-tags:before {
  content: "\e042";
}
.glyphicon-book:before {
  content: "\e043";
}
.glyphicon-bookmark:before {
  content: "\e044";
}
.glyphicon-print:before {
  content: "\e045";
}
.glyphicon-camera:before {
  content: "\e046";
}
.glyphicon-font:before {
  content: "\e047";
}
.glyphicon-bold:before {
  content: "\e048";
}
.glyphicon-italic:before {
  content: "\e049";
}
.glyphicon-text-height:before {
  content: "\e050";
}
.glyphicon-text-width:before {
  content: "\e051";
}
.glyphicon-align-left:before {
  content: "\e052";
}
.glyphicon-align-center:before {
  content: "\e053";
}
.glyphicon-align-right:before {
  content: "\e054";
}
.glyphicon-align-justify:before {
  content: "\e055";
}
.glyphicon-list:before {
  content: "\e056";
}
.glyphicon-indent-left:before {
  content: "\e057";
}
.glyphicon-indent-right:before {
  content: "\e058";
}
.glyphicon-facetime-video:before {
  content: "\e059";
}
.glyphicon-picture:before {
  content: "\e060";
}
.glyphicon-map-marker:before {
  content: "\e062";
}
.glyphicon-adjust:before {
  content: "\e063";
}
.glyphicon-tint:before {
  content: "\e064";
}
.glyphicon-edit:before {
  content: "\e065";
}
.glyphicon-share:before {
  content: "\e066";
}
.glyphicon-check:before {
  content: "\e067";
}
.glyphicon-move:before {
  content: "\e068";
}
.glyphicon-step-backward:before {
  content: "\e069";
}
.glyphicon-fast-backward:before {
  content: "\e070";
}
.glyphicon-backward:before {
  content: "\e071";
}
.glyphicon-play:before {
  content: "\e072";
}
.glyphicon-pause:before {
  content: "\e073";
}
.glyphicon-stop:before {
  content: "\e074";
}
.glyphicon-forward:before {
  content: "\e075";
}
.glyphicon-fast-forward:before {
  content: "\e076";
}
.glyphicon-step-forward:before {
  content: "\e077";
}
.glyphicon-eject:before {
  content: "\e078";
}
.glyphicon-chevron-left:before {
  content: "\e079";
}
.glyphicon-chevron-right:before {
  content: "\e080";
}
.glyphicon-plus-sign:before {
  content: "\e081";
}
.glyphicon-minus-sign:before {
  content: "\e082";
}
.glyphicon-remove-sign:before {
  content: "\e083";
}
.glyphicon-ok-sign:before {
  content: "\e084";
}
.glyphicon-question-sign:before {
  content: "\e085";
}
.glyphicon-info-sign:before {
  content: "\e086";
}
.glyphicon-screenshot:before {
  content: "\e087";
}
.glyphicon-remove-circle:before {
  content: "\e088";
}
.glyphicon-ok-circle:before {
  content: "\e089";
}
.glyphicon-ban-circle:before {
  content: "\e090";
}
.glyphicon-arrow-left:before {
  content: "\e091";
}
.glyphicon-arrow-right:before {
  content: "\e092";
}
.glyphicon-arrow-up:before {
  content: "\e093";
}
.glyphicon-arrow-down:before {
  content: "\e094";
}
.glyphicon-share-alt:before {
  content: "\e095";
}
.glyphicon-resize-full:before {
  content: "\e096";
}
.glyphicon-resize-small:before {
  content: "\e097";
}
.glyphicon-exclamation-sign:before {
  content: "\e101";
}
.glyphicon-gift:before {
  content: "\e102";
}
.glyphicon-leaf:before {
  content: "\e103";
}
.glyphicon-fire:before {
  content: "\e104";
}
.glyphicon-eye-open:before {
  content: "\e105";
}
.glyphicon-eye-close:before {
  content: "\e106";
}
.glyphicon-warning-sign:before {
  content: "\e107";
}
.glyphicon-plane:before {
  content: "\e108";
}
.glyphicon-calendar:before {
  content: "\e109";
}
.glyphicon-random:before {
  content: "\e110";
}
.glyphicon-comment:before {
  content: "\e111";
}
.glyphicon-magnet:before {
  content: "\e112";
}
.glyphicon-chevron-up:before {
  content: "\e113";
}
.glyphicon-chevron-down:before {
  content: "\e114";
}
.glyphicon-retweet:before {
  content: "\e115";
}
.glyphicon-shopping-cart:before {
  content: "\e116";
}
.glyphicon-folder-close:before {
  content: "\e117";
}
.glyphicon-folder-open:before {
  content: "\e118";
}
.glyphicon-resize-vertical:before {
  content: "\e119";
}
.glyphicon-resize-horizontal:before {
  content: "\e120";
}
.glyphicon-hdd:before {
  content: "\e121";
}
.glyphicon-bullhorn:before {
  content: "\e122";
}
.glyphicon-bell:before {
  content: "\e123";
}
.glyphicon-certificate:before {
  content: "\e124";
}
.glyphicon-thumbs-up:before {
  content: "\e125";
}
.glyphicon-thumbs-down:before {
  content: "\e126";
}
.glyphicon-hand-right:before {
  content: "\e127";
}
.glyphicon-hand-left:before {
  content: "\e128";
}
.glyphicon-hand-up:before {
  content: "\e129";
}
.glyphicon-hand-down:before {
  content: "\e130";
}
.glyphicon-circle-arrow-right:before {
  content: "\e131";
}
.glyphicon-circle-arrow-left:before {
  content: "\e132";
}
.glyphicon-circle-arrow-up:before {
  content: "\e133";
}
.glyphicon-circle-arrow-down:before {
  content: "\e134";
}
.glyphicon-globe:before {
  content: "\e135";
}
.glyphicon-wrench:before {
  content: "\e136";
}
.glyphicon-tasks:before {
  content: "\e137";
}
.glyphicon-filter:before {
  content: "\e138";
}
.glyphicon-briefcase:before {
  content: "\e139";
}
.glyphicon-fullscreen:before {
  content: "\e140";
}
.glyphicon-dashboard:before {
  content: "\e141";
}
.glyphicon-paperclip:before {
  content: "\e142";
}
.glyphicon-heart-empty:before {
  content: "\e143";
}
.glyphicon-link:before {
  content: "\e144";
}
.glyphicon-phone:before {
  content: "\e145";
}
.glyphicon-pushpin:before {
  content: "\e146";
}
.glyphicon-usd:before {
  content: "\e148";
}
.glyphicon-gbp:before {
  content: "\e149";
}
.glyphicon-sort:before {
  content: "\e150";
}
.glyphicon-sort-by-alphabet:before {
  content: "\e151";
}
.glyphicon-sort-by-alphabet-alt:before {
  content: "\e152";
}
.glyphicon-sort-by-order:before {
  content: "\e153";
}
.glyphicon-sort-by-order-alt:before {
  content: "\e154";
}
.glyphicon-sort-by-attributes:before {
  content: "\e155";
}
.glyphicon-sort-by-attributes-alt:before {
  content: "\e156";
}
.glyphicon-unchecked:before {
  content: "\e157";
}
.glyphicon-expand:before {
  content: "\e158";
}
.glyphicon-collapse-down:before {
  content: "\e159";
}
.glyphicon-collapse-up:before {
  content: "\e160";
}
.glyphicon-log-in:before {
  content: "\e161";
}
.glyphicon-flash:before {
  content: "\e162";
}
.glyphicon-log-out:before {
  content: "\e163";
}
.glyphicon-new-window:before {
  content: "\e164";
}
.glyphicon-record:before {
  content: "\e165";
}
.glyphicon-save:before {
  content: "\e166";
}
.glyphicon-open:before {
  content: "\e167";
}
.glyphicon-saved:before {
  content: "\e168";
}
.glyphicon-import:before {
  content: "\e169";
}
.glyphicon-export:before {
  content: "\e170";
}
.glyphicon-send:before {
  content: "\e171";
}
.glyphicon-floppy-disk:before {
  content: "\e172";
}
.glyphicon-floppy-saved:before {
  content: "\e173";
}
.glyphicon-floppy-remove:before {
  content: "\e174";
}
.glyphicon-floppy-save:before {
  content: "\e175";
}
.glyphicon-floppy-open:before {
  content: "\e176";
}
.glyphicon-credit-card:before {
  content: "\e177";
}
.glyphicon-transfer:before {
  content: "\e178";
}
.glyphicon-cutlery:before {
  content: "\e179";
}
.glyphicon-header:before {
  content: "\e180";
}
.glyphicon-compressed:before {
  content: "\e181";
}
.glyphicon-earphone:before {
  content: "\e182";
}
.glyphicon-phone-alt:before {
  content: "\e183";
}
.glyphicon-tower:before {
  content: "\e184";
}
.glyphicon-stats:before {
  content: "\e185";
}
.glyphicon-sd-video:before {
  content: "\e186";
}
.glyphicon-hd-video:before {
  content: "\e187";
}
.glyphicon-subtitles:before {
  content: "\e188";
}
.glyphicon-sound-stereo:before {
  content: "\e189";
}
.glyphicon-sound-dolby:before {
  content: "\e190";
}
.glyphicon-sound-5-1:before {
  content: "\e191";
}
.glyphicon-sound-6-1:before {
  content: "\e192";
}
.glyphicon-sound-7-1:before {
  content: "\e193";
}
.glyphicon-copyright-mark:before {
  content: "\e194";
}
.glyphicon-registration-mark:before {
  content: "\e195";
}
.glyphicon-cloud-download:before {
  content: "\e197";
}
.glyphicon-cloud-upload:before {
  content: "\e198";
}
.glyphicon-tree-conifer:before {
  content: "\e199";
}
.glyphicon-tree-deciduous:before {
  content: "\e200";
}
.glyphicon-cd:before {
  content: "\e201";
}
.glyphicon-save-file:before {
  content: "\e202";
}
.glyphicon-open-file:before {
  content: "\e203";
}
.glyphicon-level-up:before {
  content: "\e204";
}
.glyphicon-copy:before {
  content: "\e205";
}
.glyphicon-paste:before {
  content: "\e206";
}
.glyphicon-alert:before {
  content: "\e209";
}
.glyphicon-equalizer:before {
  content: "\e210";
}
.glyphicon-king:before {
  content: "\e211";
}
.glyphicon-queen:before {
  content: "\e212";
}
.glyphicon-pawn:before {
  content: "\e213";
}
.glyphicon-bishop:before {
  content: "\e214";
}
.glyphicon-knight:before {
  content: "\e215";
}
.glyphicon-baby-formula:before {
  content: "\e216";
}
.glyphicon-tent:before {
  content: "\26fa";
}
.glyphicon-blackboard:before {
  content: "\e218";
}
.glyphicon-bed:before {
  content: "\e219";
}
.glyphicon-apple:before {
  content: "\f8ff";
}
.glyphicon-erase:before {
  content: "\e221";
}
.glyphicon-hourglass:before {
  content: "\231b";
}
.glyphicon-lamp:before {
  content: "\e223";
}
.glyphicon-duplicate:before {
  content: "\e224";
}
.glyphicon-piggy-bank:before {
  content: "\e225";
}
.glyphicon-scissors:before {
  content: "\e226";
}
.glyphicon-bitcoin:before {
  content: "\e227";
}
.glyphicon-btc:before {
  content: "\e227";
}
.glyphicon-xbt:before {
  content: "\e227";
}
.glyphicon-yen:before {
  content: "\00a5";
}
.glyphicon-jpy:before {
  content: "\00a5";
}
.glyphicon-ruble:before {
  content: "\20bd";
}
.glyphicon-rub:before {
  content: "\20bd";
}
.glyphicon-scale:before {
  content: "\e230";
}
.glyphicon-ice-lolly:before {
  content: "\e231";
}
.glyphicon-ice-lolly-tasted:before {
  content: "\e232";
}
.glyphicon-education:before {
  content: "\e233";
}
.glyphicon-option-horizontal:before {
  content: "\e234";
}
.glyphicon-option-vertical:before {
  content: "\e235";
}
.glyphicon-menu-hamburger:before {
  content: "\e236";
}
.glyphicon-modal-window:before {
  content: "\e237";
}
.glyphicon-oil:before {
  content: "\e238";
}
.glyphicon-grain:before {
  content: "\e239";
}
.glyphicon-sunglasses:before {
  content: "\e240";
}
.glyphicon-text-size:before {
  content: "\e241";
}
.glyphicon-text-color:before {
  content: "\e242";
}
.glyphicon-text-background:before {
  content: "\e243";
}
.glyphicon-object-align-top:before {
  content: "\e244";
}
.glyphicon-object-align-bottom:before {
  content: "\e245";
}
.glyphicon-object-align-horizontal:before {
  content: "\e246";
}
.glyphicon-object-align-left:before {
  content: "\e247";
}
.glyphicon-object-align-vertical:before {
  content: "\e248";
}
.glyphicon-object-align-right:before {
  content: "\e249";
}
.glyphicon-triangle-right:before {
  content: "\e250";
}
.glyphicon-triangle-left:before {
  content: "\e251";
}
.glyphicon-triangle-bottom:before {
  content: "\e252";
}
.glyphicon-triangle-top:before {
  content: "\e253";
}
.glyphicon-conso
Download .txt
gitextract_mcc003ff/

├── .gitignore
├── README.md
├── alembic/
│   ├── README
│   ├── env.py
│   ├── script.py.mako
│   └── versions/
│       └── 753ec9bc0d27_init_v1_0.py
├── alembic.ini
├── config.py
├── controller/
│   ├── __init__.py
│   ├── admin.py
│   ├── admin_article.py
│   ├── admin_article_type.py
│   ├── admin_custom.py
│   ├── base.py
│   ├── home.py
│   └── super.py
├── docker/
│   ├── Dockerfile
│   ├── entrypoint.sh
│   ├── nginx.conf
│   └── supervisord.conf
├── extends/
│   ├── __init__.py
│   ├── cache_tornadis.py
│   ├── pub_sub_tornadis.py
│   ├── session_redis.py
│   ├── session_tornadis.py
│   ├── time_task.py
│   └── utils.py
├── log_config.py
├── main.py
├── model/
│   ├── __init__.py
│   ├── constants.py
│   ├── logined_user.py
│   ├── models.py
│   ├── pager.py
│   ├── search_params/
│   │   ├── __init__.py
│   │   ├── article_params.py
│   │   ├── article_type_params.py
│   │   ├── comment_params.py
│   │   ├── menu_params.py
│   │   └── plugin_params.py
│   └── site_info.py
├── requirements.txt
├── service/
│   ├── __init__.py
│   ├── article_service.py
│   ├── article_type_service.py
│   ├── blog_view_service.py
│   ├── comment_service.py
│   ├── custom_service.py
│   ├── init_service.py
│   ├── menu_service.py
│   ├── plugin_service.py
│   ├── pubsub_service.py
│   └── user_service.py
├── static/
│   ├── css/
│   │   ├── bootstrap-theme.css
│   │   ├── bootstrap.css
│   │   ├── common.css
│   │   └── prism.css
│   ├── js/
│   │   ├── admin.js
│   │   ├── articleDetail.js
│   │   ├── bootstrap.js
│   │   ├── floatButton.js
│   │   ├── markdown/
│   │   │   ├── bootstrap-markdown.js
│   │   │   ├── locale/
│   │   │   │   └── bootstrap-markdown.zh.js
│   │   │   ├── markdown.js
│   │   │   └── to-markdown.js
│   │   ├── markdownEdit.js
│   │   ├── npm.js
│   │   ├── super.js
│   │   └── tinymce_setup.js
│   └── tinymce/
│       ├── LICENSE.TXT
│       ├── changelog.txt
│       └── js/
│           └── tinymce/
│               ├── extentsion_self/
│               │   └── codesimple_extentsion/
│               │       └── prism.js
│               ├── langs/
│               │   ├── readme.md
│               │   └── zh_CN.js
│               ├── license.txt
│               ├── plugins/
│               │   ├── codesample/
│               │   │   └── css/
│               │   │       └── prism.css
│               │   ├── example/
│               │   │   └── dialog.html
│               │   ├── media/
│               │   │   └── moxieplayer.swf
│               │   └── visualblocks/
│               │       └── css/
│               │           └── visualblocks.css
│               └── skins/
│                   └── myskin/
│                       ├── Variables.less
│                       ├── fonts/
│                       │   ├── readme.md
│                       │   ├── tinymce-small.json
│                       │   └── tinymce.json
│                       └── skin.json
├── template/
│   ├── 403.html
│   ├── 404.html
│   ├── 500.html
│   ├── _article_comments.html
│   ├── _macros.html
│   ├── admin/
│   │   ├── admin_account.html
│   │   ├── admin_base.html
│   │   ├── blog_plugin_add.html
│   │   ├── blog_plugin_edit.html
│   │   ├── custom_blog_info.html
│   │   ├── custom_blog_plugin.html
│   │   ├── help_page.html
│   │   ├── manage_articleTypes.html
│   │   ├── manage_articleTypes_nav.html
│   │   ├── manage_articles.html
│   │   ├── manage_comments.html
│   │   └── submit_articles.html
│   ├── article_detials.html
│   ├── auth/
│   │   └── login.html
│   ├── base.html
│   ├── index.html
│   └── super/
│       └── init.html
└── url_mapping.py
Download .txt
SYMBOL INDEX (363 symbols across 45 files)

FILE: alembic/env.py
  function run_migrations_offline (line 23) | def run_migrations_offline():
  function run_migrations_online (line 43) | def run_migrations_online():

FILE: alembic/versions/753ec9bc0d27_init_v1_0.py
  function upgrade (line 20) | def upgrade():
  function downgrade (line 153) | def downgrade():

FILE: controller/admin.py
  class AdminAccountHandler (line 8) | class AdminAccountHandler(BaseHandler):
    method get (line 11) | def get(self):
    method post (line 15) | def post(self, require):
    method edit_user_info (line 23) | def edit_user_info(self):
    method change_password (line 36) | def change_password(self):
  class AdminHelpHandler (line 48) | class AdminHelpHandler(BaseHandler):
    method get (line 51) | def get(self):

FILE: controller/admin_article.py
  class ArticleAndCommentsFlush (line 18) | class ArticleAndCommentsFlush(object):
    method flush_article_cache (line 20) | def flush_article_cache(self, action, article):
    method flush_comments_cache (line 25) | def flush_comments_cache(self, action, comments):
  class AdminArticleHandler (line 30) | class AdminArticleHandler(BaseHandler, ArticleAndCommentsFlush):
    method get (line 33) | def get(self, *require):
    method post (line 46) | def post(self, *require):
    method page_get (line 62) | def page_get(self):
    method article_get (line 72) | def article_get(self, article_id):
    method submit_get (line 79) | def submit_get(self):
    method submit_post (line 95) | def submit_post(self):
    method update_post (line 115) | def update_post(self, article_id):
    method delete_post (line 136) | def delete_post(self, article_id):
  class AdminArticleCommentHandler (line 148) | class AdminArticleCommentHandler(BaseHandler, ArticleAndCommentsFlush):
    method get (line 150) | def get(self, *require):
    method post (line 154) | def post(self, *require):
    method page_get (line 169) | def page_get(self):
    method disable_post (line 179) | def disable_post(self, article_id, comment_id, disabled):
    method delete_post (line 190) | def delete_post(self, article_id, comment_id):

FILE: controller/admin_article_type.py
  class AdminArticleTypeBaseHandler (line 13) | class AdminArticleTypeBaseHandler(BaseHandler):
    method flush_menus (line 15) | def flush_menus(self, menus=None, article_types_not_under_menu=None):
  class AdminArticleTypeHandler (line 25) | class AdminArticleTypeHandler(AdminArticleTypeBaseHandler):
    method get (line 28) | def get(self, *require):
    method post (line 39) | def post(self, *require):
    method page_get (line 52) | def page_get(self):
    method delete_get (line 63) | def delete_get(self, article_type_id):
    method add_post (line 77) | def add_post(self):
    method update_post (line 99) | def update_post(self, article_type_id):
  class AdminArticleTypeNavHandler (line 121) | class AdminArticleTypeNavHandler(AdminArticleTypeBaseHandler):
    method get (line 124) | def get(self, *require):
    method post (line 139) | def post(self, *require):
    method add_post (line 152) | def add_post(self):
    method update_post (line 167) | def update_post(self, menu_id):
    method page_get (line 182) | def page_get(self):
    method sort_up_get (line 190) | def sort_up_get(self, menu_id):
    method sort_down_get (line 204) | def sort_down_get(self, menu_id):
    method sort_up_get (line 218) | def sort_up_get(self, menu_id):
    method delete_get (line 232) | def delete_get(self, menu_id):

FILE: controller/admin_custom.py
  class AdminCustomBlogInfoHandler (line 13) | class AdminCustomBlogInfoHandler(BaseHandler):
    method get (line 16) | def get(self):
    method post (line 21) | def post(self):
    method flush_blog_info (line 34) | def flush_blog_info(self, blog_info):
  class AdminCustomBlogPluginHandler (line 40) | class AdminCustomBlogPluginHandler(BaseHandler):
    method get (line 43) | def get(self, *require):
    method post (line 67) | def post(self, *require):
    method index_get (line 80) | def index_get(self):
    method add_get (line 87) | def add_get(self):
    method edit_get (line 92) | def edit_get(self, plugin_id):
    method sort_up_get (line 98) | def sort_up_get(self, plugin_id):
    method sort_down_get (line 109) | def sort_down_get(self, plugin_id):
    method set_disabled_get (line 120) | def set_disabled_get(self, plugin_id, disabled):
    method delete_get (line 131) | def delete_get(self, plugin_id):
    method add_post (line 142) | def add_post(self):
    method edit_post (line 155) | def edit_post(self, plugin_id):
    method flush_plugins (line 172) | def flush_plugins(self, plugins=None):

FILE: controller/base.py
  class BaseHandler (line 17) | class BaseHandler(tornado.web.RequestHandler):
    method initialize (line 19) | def initialize(self):
    method login_url (line 28) | def login_url(self):
    method prepare (line 32) | def prepare(self):
    method add_pv_uv (line 42) | def add_pv_uv(self):
    method init_session (line 55) | def init_session(self):
    method save_session (line 60) | def save_session(self):
    method db (line 65) | def db(self):
    method pubsub_manager (line 71) | def pubsub_manager(self):
    method save_login_user (line 74) | def save_login_user(self, user):
    method logout (line 84) | def logout(self):
    method has_message (line 90) | def has_message(self):
    method add_message (line 97) | def add_message(self, category, message):
    method read_messages (line 106) | def read_messages(self):
    method write_json (line 113) | def write_json(self, json):
    method write_error (line 117) | def write_error(self, status_code, **kwargs):
    method get_gravatar_url (line 128) | def get_gravatar_url(self, email, default=None, size=40):
    method on_finish (line 139) | def on_finish(self):

FILE: controller/home.py
  class HomeHandler (line 15) | class HomeHandler(BaseHandler):
    method get (line 17) | def get(self):
  class ArticleHandler (line 29) | class ArticleHandler(BaseHandler):
    method get (line 31) | def get(self, article_id):
  class ArticleCommentHandler (line 43) | class ArticleCommentHandler(BaseHandler, ArticleAndCommentsFlush):
    method post (line 45) | def post(self, article_id):
  class ArticleTypeHandler (line 69) | class ArticleTypeHandler(BaseHandler):
    method get (line 71) | def get(self, type_id):
  class articleSourceHandler (line 84) | class articleSourceHandler(BaseHandler):
    method get (line 86) | def get(self, source_id):
  class LoginHandler (line 99) | class LoginHandler(BaseHandler):
    method get (line 101) | def get(self):
    method post (line 106) | def post(self):
  class LogoutHandler (line 120) | class LogoutHandler(BaseHandler):
    method get (line 122) | def get(self):

FILE: controller/super.py
  class SuperHandler (line 8) | class SuperHandler(BaseHandler):
    method get (line 10) | def get(self):
    method post (line 18) | def post(self):

FILE: extends/cache_tornadis.py
  class CacheManager (line 10) | class CacheManager(object):
    method __init__ (line 11) | def __init__(self, options):
    method get_connection_pool (line 16) | def get_connection_pool(self):
    method get_redis_client (line 24) | def get_redis_client(self):
    method fetch_client (line 33) | def fetch_client(self):
    method call (line 37) | def call(self, *args, **kwargs):
    method call (line 47) | def call(self, *args, **kwargs):
    method call_watch_transaction (line 57) | def call_watch_transaction(self, watch_key, *args, **kwargs):

FILE: extends/pub_sub_tornadis.py
  class PubSubTornadis (line 10) | class PubSubTornadis(object):
    method __init__ (line 12) | def __init__(self, redis_pub_sub_config, loop=None):
    method get_client (line 23) | def get_client(self):
    method get_pub_client (line 29) | def get_pub_client(self):
    method pub_call (line 38) | def pub_call(self, msg, *channels):
    method long_listen (line 47) | def long_listen(self):
    method connect_and_listen (line 51) | def connect_and_listen(self, channels):
    method first_do_after_subscribed (line 78) | def first_do_after_subscribed(self):
    method do_msg (line 83) | def do_msg(self, msgs):

FILE: extends/session_redis.py
  class Session (line 8) | class Session(dict):
    method __init__ (line 9) | def __init__(self, request_handler):
    method get_session_id (line 17) | def get_session_id(self):
    method generate_session_id (line 22) | def generate_session_id(self):
    method fetch_client (line 27) | def fetch_client(self):
    method save (line 33) | def save(self):
  class SessionManager (line 41) | class SessionManager(object):
    method __init__ (line 42) | def __init__(self, options):
    method get_connection_pool (line 48) | def get_connection_pool(self):
    method get_redis_client (line 55) | def get_redis_client(self):

FILE: extends/session_tornadis.py
  class Session (line 11) | class Session(dict):
    method __init__ (line 12) | def __init__(self, request_handler):
    method init_fetch (line 20) | def init_fetch(self):
    method get_session_id (line 24) | def get_session_id(self):
    method generate_session_id (line 29) | def generate_session_id(self):
    method fetch_client (line 37) | def fetch_client(self):
    method save (line 44) | def save(self, expire_time=None):
    method call_client (line 52) | def call_client(self, *args, **kwargs):
  class SessionManager (line 61) | class SessionManager(object):
    method __init__ (line 62) | def __init__(self, options):
    method get_connection_pool (line 68) | def get_connection_pool(self):
    method get_redis_client (line 76) | def get_redis_client(self):

FILE: extends/time_task.py
  class TimeTask (line 8) | class TimeTask(object):
    method __init__ (line 9) | def __init__(self, sqlalchemy_engine):
    method add_cache_flush_task (line 13) | def add_cache_flush_task(self, func, *args, **kwargs):
    method start_tasks (line 18) | def start_tasks(self):

FILE: extends/utils.py
  function singleton (line 9) | def singleton(cls, *args, **kw):
  class AlchemyEncoder (line 19) | class AlchemyEncoder(json.JSONEncoder):
    method __init__ (line 21) | def __init__(self, dumps_objs=None, *w, **kw):
    method default (line 27) | def default(self, o):
  class Dict (line 47) | class Dict(dict):
    method __getattr__ (line 48) | def __getattr__(self, key):
    method __setattr__ (line 57) | def __setattr__(self, key, value):

FILE: log_config.py
  function init (line 15) | def init(port, console_handler=False, file_handler=True, log_path=FILE['...

FILE: main.py
  function db_poll_init (line 33) | def db_poll_init():
  function cache_manager_init (line 41) | def cache_manager_init():
  class Application (line 47) | class Application(tornado.web.Application):
    method __init__ (line 48) | def __init__(self):
  function parse_command_line (line 58) | def parse_command_line():

FILE: model/constants.py
  class Constants (line 4) | class Constants(object):

FILE: model/logined_user.py
  class LoginUser (line 5) | class LoginUser(Dict):
    method __init__ (line 11) | def __init__(self, user):

FILE: model/models.py
  class DbInit (line 11) | class DbInit(object):
  class User (line 15) | class User(DbBase,DbInit):
    method verify_password (line 22) | def verify_password(self, password):
  class Menu (line 26) | class Menu(DbBase):
    method fetch_all_types (line 33) | def fetch_all_types(self, only_show_not_hide=False):
    method __repr__ (line 41) | def __repr__(self):
  class ArticleTypeSetting (line 45) | class ArticleTypeSetting(DbBase):
    method return_setting_hide (line 54) | def return_setting_hide():
    method __repr__ (line 57) | def __repr__(self):
  class ArticleType (line 61) | class ArticleType(DbBase):
    method is_protected (line 71) | def is_protected(self):
    method is_hide (line 78) | def is_hide(self):
    method fetch_articles_count (line 84) | def fetch_articles_count(self):
    method __repr__ (line 89) | def __repr__(self):
  class Source (line 93) | class Source(DbBase):
    method fetch_articles_count (line 99) | def fetch_articles_count(self):
    method __repr__ (line 102) | def __repr__(self):
  class Comment (line 106) | class Comment(DbBase):
  class Article (line 122) | class Article(DbBase):
    method fetch_comments_count (line 135) | def fetch_comments_count(self, count=None):
    method __repr__ (line 138) | def __repr__(self):
  class BlogInfo (line 142) | class BlogInfo(DbBase):
  class Plugin (line 150) | class Plugin(DbBase):
    method __repr__ (line 159) | def __repr__(self):
  class BlogView (line 163) | class BlogView(DbBase):

FILE: model/pager.py
  class Pager (line 5) | class Pager(Dict):
    method __init__ (line 9) | def __init__(self, request):
    method build_query (line 16) | def build_query(self, query):
    method set_total_count (line 24) | def set_total_count(self, count):
    method set_result (line 29) | def set_result(self, result):
    method has_prev (line 33) | def has_prev(self):
    method has_next (line 36) | def has_next(self):
    method build_url (line 39) | def build_url(self, url, page_no, params):

FILE: model/search_params/article_params.py
  class ArticleSearchParams (line 2) | class ArticleSearchParams(object):
    method __init__ (line 6) | def __init__(self, request):
    method to_url_params (line 16) | def to_url_params(self):

FILE: model/search_params/article_type_params.py
  class ArticleTypeSearchParams (line 2) | class ArticleTypeSearchParams(object):
    method __init__ (line 6) | def __init__(self, request):

FILE: model/search_params/comment_params.py
  class CommentSearchParams (line 2) | class CommentSearchParams(object):
    method __init__ (line 7) | def __init__(self, request):

FILE: model/search_params/menu_params.py
  class MenuSearchParams (line 2) | class MenuSearchParams(object):
    method __init__ (line 6) | def __init__(self, request):

FILE: model/search_params/plugin_params.py
  class PluginSearchParams (line 4) | class PluginSearchParams(object):
    method __init__ (line 8) | def __init__(self, request):

FILE: model/site_info.py
  class SiteCollection (line 4) | class SiteCollection(object):

FILE: service/__init__.py
  class BaseService (line 4) | class BaseService(object):
    method query_pager (line 6) | def query_pager(query, pager, count=None):

FILE: service/article_service.py
  class ArticleService (line 16) | class ArticleService(object):
    method get_article_all (line 21) | def get_article_all(db_session, article_id, show_source_type=False, ad...
    method page_articles (line 34) | def page_articles(db_session, pager, search_params):
    method add_article (line 69) | def add_article(db_session, article):
    method update_article (line 85) | def update_article(db_session, article):
    method delete_article (line 106) | def delete_article(db_session, article_id):
    method get_core_content (line 119) | def get_core_content(content, limit=0):
    method get_count (line 126) | def get_count(db_session):
    method get_article_sources (line 132) | def get_article_sources(db_session):
    method set_article_type_default_by_article_type_id (line 140) | def set_article_type_default_by_article_type_id(db_session, article_ty...

FILE: service/article_type_service.py
  class ArticleTypeService (line 12) | class ArticleTypeService(object):
    method page_article_types (line 14) | def page_article_types(db_session, pager, search_params):
    method list_article_types_not_under_menu (line 29) | def list_article_types_not_under_menu(db_session):
    method add_article_type (line 36) | def add_article_type(db_session, article_type):
    method update_article_type (line 50) | def update_article_type(db_session, article_type_id, article_type):
    method delete (line 69) | def delete(db_session, article_type_id):
    method set_article_type_menu_id_none (line 81) | def set_article_type_menu_id_none(db_session, menu_id, auto_commit=True):
    method list_simple (line 87) | def list_simple(db_session):

FILE: service/blog_view_service.py
  class BlogViewService (line 9) | class BlogViewService(object):
    method get_blog_view (line 11) | def get_blog_view(db_session, date=None):
    method add_blog_view (line 18) | def add_blog_view(db_session, add_pv, add_uv, date=None):

FILE: service/comment_service.py
  class CommentService (line 13) | class CommentService(object):
    method get_comment (line 15) | def get_comment(db_session, comment_id):
    method get_max_floor (line 19) | def get_max_floor(db_session, article_id):
    method add_comment (line 24) | def add_comment(db_session, article_id, comment):
    method update_comment_disabled (line 36) | def update_comment_disabled(db_session, article_id, comment_id, disabl...
    method delete_comment (line 43) | def delete_comment(db_session, article_id, comment_id):
    method page_comments (line 52) | def page_comments(db_session, pager, params):
    method remove_by_article_id (line 67) | def remove_by_article_id(db_session, article_id, commit=True):
    method get_comment_count (line 79) | def get_comment_count(db_session):
    method get_comments_count_subquery (line 84) | def get_comments_count_subquery(db_session):

FILE: service/custom_service.py
  class BlogInfoService (line 9) | class BlogInfoService(object):
    method get_blog_info (line 12) | def get_blog_info(db_session):
    method update_blog_info (line 17) | def update_blog_info(db_session, blog_info):

FILE: service/init_service.py
  class SiteCacheService (line 27) | class SiteCacheService(object):
    method query_all (line 47) | def query_all(cache_manager, thread_do, db, is_pub_all=False, pubsub_m...
    method query_blog_info (line 58) | def query_blog_info(cache_manager, thread_do, db, is_pub_all=False, pu...
    method query_menus (line 72) | def query_menus(cache_manager, thread_do, db, is_pub_all=False, pubsub...
    method query_plugins (line 87) | def query_plugins(cache_manager, thread_do, db, is_pub_all=False, pubs...
    method query_article_count (line 98) | def query_article_count(cache_manager, thread_do, db, is_pub_all=False...
    method query_article_sources (line 109) | def query_article_sources(cache_manager, thread_do, db, is_pub_all=Fal...
    method query_source_articles_count (line 124) | def query_source_articles_count(cache_manager, source_id=None):
    method query_comment_count (line 135) | def query_comment_count(cache_manager, thread_do, db, is_pub_all=False...
    method query_blog_view_count (line 146) | def query_blog_view_count(cache_manager, thread_do, db, is_pub_all=Fal...
    method update_by_sub_msg (line 162) | def update_by_sub_msg(msgs, cache_manager, thread_do, db):
    method update_blog_info (line 190) | def update_blog_info(cache_manager, blog_info, is_pub_all=False, pubsu...
    method update_plugins (line 202) | def update_plugins(cache_manager, plugins, is_pub_all=False, pubsub_ma...
    method update_menus (line 212) | def update_menus(cache_manager, menus, article_types_not_under_menu, i...
    method update_article_count (line 226) | def update_article_count(cache_manager, article_count, is_pub_all=Fals...
    method update_comment_count (line 235) | def update_comment_count(cache_manager, comment_count, is_pub_all=Fals...
    method update_blog_view_count (line 244) | def update_blog_view_count(cache_manager, pv, uv, is_pub_all=False, pu...
    method update_article_sources (line 255) | def update_article_sources(cache_manager, article_sources, is_pub_all=...
    method update_article_action (line 270) | def update_article_action(cache_manager, action, article, is_pub_all=F...
    method update_comment_action (line 331) | def update_comment_action(cache_manager, action, comments, is_pub_all=...
    method add_pv_uv (line 349) | def add_pv_uv(cache_manager, add_pv, add_uv, is_pub_all=False, pubsub_...
  function flush_all_cache (line 365) | def flush_all_cache():
  function get_all_site_cache_keys (line 375) | def get_all_site_cache_keys():

FILE: service/menu_service.py
  class MenuService (line 14) | class MenuService(object):
    method page_menus (line 16) | def page_menus(db_session, pager, search_params):
    method add_menu (line 28) | def add_menu(db_session, menu):
    method get_max_order (line 40) | def get_max_order(db_session):
    method list_menus (line 47) | def list_menus(db_session, show_types=False):
    method sort_up (line 58) | def sort_up(db_session, menu_id):
    method sort_down (line 72) | def sort_down(db_session, menu_id):
    method update (line 86) | def update(db_session, menu_id, menu_to_update):
    method delete (line 97) | def delete(db_session, menu_id):

FILE: service/plugin_service.py
  class PluginService (line 11) | class PluginService(object):
    method get (line 14) | def get(db_session, plugin_id):
    method get_editable (line 19) | def get_editable(db_session, plugin_id):
    method list_plugins (line 26) | def list_plugins(db_session):
    method page_plugins (line 31) | def page_plugins(db_session, pager, search_params):
    method save (line 40) | def save(db_session, plugin):
    method get_max_order (line 52) | def get_max_order(db_session):
    method sort_up (line 59) | def sort_up(db_session, plugin_id):
    method sort_down (line 73) | def sort_down(db_session, plugin_id):
    method update_disabled (line 87) | def update_disabled(db_session, plugin_id, disabled):
    method delete (line 94) | def delete(db_session, plugin_id):
    method update (line 103) | def update(db_session, plugin_id, plugin_to_update):

FILE: service/pubsub_service.py
  class PubSubService (line 11) | class PubSubService(PubSubTornadis):
    method __init__ (line 13) | def __init__(self, redis_pub_sub_config, application, loop=None):
    method db (line 23) | def db(self):
    method first_do_after_subscribed (line 29) | def first_do_after_subscribed(self):
    method do_msg (line 33) | def do_msg(self, msgs):

FILE: service/user_service.py
  class UserService (line 5) | class UserService(object):
    method get_user (line 8) | def get_user(db_session, username):
    method update_user_info (line 12) | def update_user_info(db_session, username, password, user):
    method update_password (line 25) | def update_password(db_session, username, old_password, new_password):
    method get_count (line 32) | def get_count(db_session):
    method save_user (line 36) | def save_user(db_session, user):

FILE: static/js/admin.js
  function delCfm (line 13) | function delCfm(delLink) {
  function delCommentCfm (line 45) | function delCommentCfm(url) {
  function pop_commentForm (line 72) | function pop_commentForm(followId, articleId) {
  function delArticleTypeCfm (line 86) | function delArticleTypeCfm(url) {
  function get_articleType_info (line 94) | function get_articleType_info(url, id, name, is_hide, introduction, menu...
  function get_articleTypeNav_info (line 120) | function get_articleTypeNav_info(url, id, name) {
  function delArticleTypeNavCfm (line 129) | function delArticleTypeNavCfm(url) {
  function get_blog_info (line 137) | function get_blog_info() {
  function delPluginCfm (line 142) | function delPluginCfm(url) {
  function changePassword (line 150) | function changePassword() {
  function editUserInfo (line 155) | function editUserInfo() {
  function checkChangePasswordForm (line 160) | function checkChangePasswordForm() {
  function update_comment (line 173) | function update_comment(url) {
  function delCommentCfm (line 180) | function delCommentCfm(url) {
  function replyComment (line 187) | function replyComment(action, reply_to_id, reply_to_floor) {

FILE: static/js/articleDetail.js
  function update_disable (line 1) | function update_disable(url) {
  function delete_comment (line 8) | function delete_comment(url) {
  function delCommentCfm (line 15) | function delCommentCfm(url) {
  function go_to_reply (line 22) | function go_to_reply(comment_type, reply_to_id, reply_to_floor) {

FILE: static/js/bootstrap.js
  function transitionEnd (line 34) | function transitionEnd() {
  function removeElement (line 126) | function removeElement() {
  function Plugin (line 142) | function Plugin(option) {
  function Plugin (line 251) | function Plugin(option) {
  function Plugin (line 470) | function Plugin(option) {
  function getTargetFromTrigger (line 689) | function getTargetFromTrigger($trigger) {
  function Plugin (line 701) | function Plugin(option) {
  function getParent (line 768) | function getParent($this) {
  function clearMenus (line 781) | function clearMenus(e) {
  function Plugin (line 874) | function Plugin(option) {
  function Plugin (line 1200) | function Plugin(option, _relatedTarget) {
  function complete (line 1566) | function complete() {
  function Plugin (line 1736) | function Plugin(option) {
  function Plugin (line 1845) | function Plugin(option) {
  function ScrollSpy (line 1888) | function ScrollSpy(element, options) {
  function Plugin (line 2008) | function Plugin(option) {
  function next (line 2117) | function next() {
  function Plugin (line 2163) | function Plugin(option) {
  function Plugin (line 2320) | function Plugin(option) {

FILE: static/js/floatButton.js
  function getCookie (line 2) | function getCookie(name) {
  function getAlertHtml (line 7) | function getAlertHtml(category, message) {
  function alertXtg (line 14) | function alertXtg(category, message, timeout) {
  function codeHighLight (line 24) | function codeHighLight() {

FILE: static/js/markdown/markdown.js
  function mk_block_toSource (line 118) | function mk_block_toSource() {
  function mk_block_inspect (line 129) | function mk_block_inspect() {
  function count_lines (line 157) | function count_lines( str ) {
  function regex_for_depth (line 434) | function regex_for_depth( depth ) {
  function expand_tab (line 443) | function expand_tab( input ) {
  function add (line 449) | function add(li, loose, inline, nl) {
  function get_contained_blocks (line 476) | function get_contained_blocks( depth, blocks ) {
  function paragraphify (line 498) | function paragraphify(s, i, stack) {
  function make_list (line 521) | function make_list( m ) {
  function add (line 806) | function add(x) {
  function strong_em (line 1016) | function strong_em( tag, md ) {
  function Block (line 1142) | function Block() {}
  function Inline (line 1144) | function Inline() {}
  function split_meta_hash (line 1184) | function split_meta_hash( meta_string ) {
  function extract_attr (line 1472) | function extract_attr( jsonml ) {
  function escapeHTML (line 1520) | function escapeHTML( text ) {
  function render_tree (line 1528) | function render_tree( jsonml ) {
  function convert_tree_to_html (line 1560) | function convert_tree_to_html( tree, references, options ) {
  function merge_text_nodes (line 1706) | function merge_text_nodes( jsonml ) {

FILE: static/js/markdown/to-markdown.js
  function replaceEls (line 87) | function replaceEls(html, elProperties) {
  function attrRegExp (line 102) | function attrRegExp(attr) {
  function replaceLists (line 129) | function replaceLists(html) {
  function replaceBlockquotes (line 161) | function replaceBlockquotes(html) {
  function cleanUp (line 172) | function cleanUp(string) {

FILE: static/js/super.js
  function checkPasswordForm (line 1) | function checkPasswordForm() {
Condensed preview — 107 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,155K chars).
[
  {
    "path": ".gitignore",
    "chars": 5,
    "preview": "logs\n"
  },
  {
    "path": "README.md",
    "chars": 3829,
    "preview": "[blog_xtg](https://github.com/xtg20121013/blog_xtg)是我个人写的一个开源分布式博客,其web框架使用的是tornado(一个基于异步IO的python web框架)。同时我把它设计成一个可以"
  },
  {
    "path": "alembic/README",
    "chars": 38,
    "preview": "Generic single-database configuration."
  },
  {
    "path": "alembic/env.py",
    "chars": 1910,
    "preview": "from __future__ import with_statement\nfrom alembic import context\nfrom sqlalchemy import engine_from_config, pool\nfrom c"
  },
  {
    "path": "alembic/script.py.mako",
    "chars": 494,
    "preview": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom ale"
  },
  {
    "path": "alembic/versions/753ec9bc0d27_init_v1_0.py",
    "chars": 6784,
    "preview": "# coding=utf-8\n\"\"\"init_v1_0\n\nRevision ID: 753ec9bc0d27\nRevises: \nCreate Date: 2017-03-12 20:17:20.958379\n\n\"\"\"\nfrom alemb"
  },
  {
    "path": "alembic.ini",
    "chars": 1462,
    "preview": "# A generic, single database configuration.\n\n[alembic]\n# path to migration scripts\nscript_location = alembic\n\n# template"
  },
  {
    "path": "config.py",
    "chars": 2945,
    "preview": "# coding=utf-8\nfrom urllib import quote_plus as urlquote\n\ncookie_keys = dict(\n    session_key_name=\"TR_SESSION_ID\",\n    "
  },
  {
    "path": "controller/__init__.py",
    "chars": 15,
    "preview": "# coding=utf-8\n"
  },
  {
    "path": "controller/admin.py",
    "chars": 1735,
    "preview": "# coding=utf-8\nfrom base import BaseHandler\nfrom tornado.gen import coroutine\nfrom tornado.web import authenticated\nfrom"
  },
  {
    "path": "controller/admin_article.py",
    "chars": 8465,
    "preview": "# coding=utf-8\nfrom tornado.gen import coroutine\nfrom tornado.web import authenticated\n\nfrom base import BaseHandler\nfro"
  },
  {
    "path": "controller/admin_article_type.py",
    "chars": 9188,
    "preview": "# coding=utf-8\nfrom tornado.web import authenticated\nfrom tornado.gen import coroutine\nfrom base import BaseHandler\nfrom"
  },
  {
    "path": "controller/admin_custom.py",
    "chars": 6676,
    "preview": "# coding=utf-8\nfrom base import BaseHandler\nfrom tornado.gen import coroutine\nfrom tornado.web import authenticated\nfrom"
  },
  {
    "path": "controller/base.py",
    "chars": 5219,
    "preview": "# coding=utf-8\nimport hashlib\nimport urllib\n\nimport datetime\nimport tornado.web\nfrom tornado import gen\nfrom tornado.esc"
  },
  {
    "path": "controller/home.py",
    "chars": 5251,
    "preview": "# coding=utf-8\nfrom tornado import gen\n\nfrom base import BaseHandler\nfrom admin_article import ArticleAndCommentsFlush\nf"
  },
  {
    "path": "controller/super.py",
    "chars": 943,
    "preview": "# coding=utf-8\nfrom tornado import gen\n\nfrom base import BaseHandler\nfrom service.user_service import UserService\n\n\nclas"
  },
  {
    "path": "docker/Dockerfile",
    "chars": 5677,
    "preview": "FROM python:2.7.13-alpine\nMAINTAINER xtg <imgamermhq@gmail.com>\n\n#时区问题(alpine解决方案)\nRUN apk update && apk add ca-certific"
  },
  {
    "path": "docker/entrypoint.sh",
    "chars": 102,
    "preview": "#!/bin/sh\nset -e\n\nif [ \"$1\" == \"upgradedb\" ]\nthen\n    python main.py upgradedb\nfi\nexec supervisord -n\n"
  },
  {
    "path": "docker/nginx.conf",
    "chars": 1535,
    "preview": "#user  xtg;\nworker_processes  2;\n\nerror_log  /var/log/nginx/error.log warn;\npid        /var/run/nginx.pid;\n\n\nevents {\n  "
  },
  {
    "path": "docker/supervisord.conf",
    "chars": 9485,
    "preview": "; Sample supervisor config file.\n;\n; For more information on the config file, please see:\n; http://supervisord.org/confi"
  },
  {
    "path": "extends/__init__.py",
    "chars": 15,
    "preview": "# coding=utf-8\n"
  },
  {
    "path": "extends/cache_tornadis.py",
    "chars": 2452,
    "preview": "# coding: utf-8\nimport logging\n\nimport tornadis\nimport tornado.gen\n\nlogger = logging.getLogger(__name__)\n\n\nclass CacheMa"
  },
  {
    "path": "extends/pub_sub_tornadis.py",
    "chars": 3202,
    "preview": "# coding=utf-8\nimport tornado.ioloop\nimport tornado.gen\nimport tornadis\nimport logging\n\nlogger = logging.getLogger(__nam"
  },
  {
    "path": "extends/session_redis.py",
    "chars": 2168,
    "preview": "# coding: utf-8\nimport uuid\nimport json\nimport redis\n\n\n# 同步的redis客户端实现,不适用tornado,暂时弃用.\nclass Session(dict):\n    def __i"
  },
  {
    "path": "extends/session_tornadis.py",
    "chars": 3039,
    "preview": "# coding: utf-8\nimport uuid\nimport json\nimport tornadis\nimport tornado.gen\nimport logging\n\nlogger = logging.getLogger(__"
  },
  {
    "path": "extends/time_task.py",
    "chars": 616,
    "preview": "# coding=utf-8\nimport logging\nfrom apscheduler.schedulers.tornado import TornadoScheduler\n\nlogger = logging.getLogger(__"
  },
  {
    "path": "extends/utils.py",
    "chars": 1749,
    "preview": "# coding=utf-8\nimport json\nimport logging\nfrom sqlalchemy.ext.declarative import DeclarativeMeta\n\nlogger = logging.getLo"
  },
  {
    "path": "log_config.py",
    "chars": 1190,
    "preview": "# coding=utf-8\nimport logging\nimport logging.handlers\nimport tornado.log\n\nFILE = dict(\n    log_path=\"logs/log\", # 末尾自动添加"
  },
  {
    "path": "main.py",
    "chars": 5552,
    "preview": "# coding=utf-8\nimport os, sys\n\nimport concurrent.futures\nimport tornado.ioloop\nfrom sqlalchemy import create_engine\nfrom"
  },
  {
    "path": "model/__init__.py",
    "chars": 15,
    "preview": "# coding=utf-8\n"
  },
  {
    "path": "model/constants.py",
    "chars": 498,
    "preview": "# coding=utf-8\n\n\nclass Constants(object):\n    SYSTEM_PLUGIN = \"system_plugin\"\n\n    COMMENT_RANK_ADMIN = \"admin\"\n    COMM"
  },
  {
    "path": "model/logined_user.py",
    "chars": 655,
    "preview": "# coding=utf-8\nfrom extends.utils import Dict\n\n\nclass LoginUser(Dict):\n        # self['id'] = None\n        # self['name'"
  },
  {
    "path": "model/models.py",
    "chars": 5523,
    "preview": "# coding: utf-8\nfrom datetime import datetime\nfrom model.constants import Constants\nfrom sqlalchemy.orm import contains_"
  },
  {
    "path": "model/pager.py",
    "chars": 1658,
    "preview": "# coding=utf-8\nfrom extends.utils import Dict\n\n\nclass Pager(Dict):\n\n    DEFAULT_PAGE_SIZE = 10\n\n    def __init__(self, r"
  },
  {
    "path": "model/search_params/__init__.py",
    "chars": 15,
    "preview": "# coding=utf-8\n"
  },
  {
    "path": "model/search_params/article_params.py",
    "chars": 834,
    "preview": "# coding=utf-8\nclass ArticleSearchParams(object):\n\n    ORDER_MODE_CREATE_TIME_DESC = 1\n\n    def __init__(self, request):"
  },
  {
    "path": "model/search_params/article_type_params.py",
    "chars": 296,
    "preview": "# coding=utf-8\nclass ArticleTypeSearchParams(object):\n\n    ORDER_MODE_ID_DESC = 1\n\n    def __init__(self, request):\n    "
  },
  {
    "path": "model/search_params/comment_params.py",
    "chars": 375,
    "preview": "# coding=utf-8\nclass CommentSearchParams(object):\n\n    ORDER_MODE_CREATE_TIME_ASC = 1\n    ORDER_MODE_CREATE_TIME_DESC = "
  },
  {
    "path": "model/search_params/menu_params.py",
    "chars": 211,
    "preview": "# coding=utf-8\nclass MenuSearchParams(object):\n\n    ORDER_MODE_ORDER_ASC = 1\n\n    def __init__(self, request):\n        s"
  },
  {
    "path": "model/search_params/plugin_params.py",
    "chars": 217,
    "preview": "# coding=utf-8\n\n\nclass PluginSearchParams(object):\n\n    ORDER_MODE_ORDER_ASC = 1\n\n    def __init__(self, request):\n     "
  },
  {
    "path": "model/site_info.py",
    "chars": 537,
    "preview": "# coding=utf-8\n\n\nclass SiteCollection(object):\n    title = None                # string\n    signature = None            "
  },
  {
    "path": "requirements.txt",
    "chars": 129,
    "preview": "tornado==4.4.2\nsqlalchemy==1.0.15\ntornadis==0.8.0\nfutures==3.0.5\nalembic==0.9.1\napscheduler==3.3.1\nmysql-connector-pytho"
  },
  {
    "path": "service/__init__.py",
    "chars": 345,
    "preview": "# coding=utf-8\n\n\nclass BaseService(object):\n    @staticmethod\n    def query_pager(query, pager, count=None):\n        if "
  },
  {
    "path": "service/article_service.py",
    "chars": 6460,
    "preview": "# coding=utf-8\nimport logging\nimport re\n\nfrom model.site_info import SiteCollection\nfrom sqlalchemy.orm import joinedloa"
  },
  {
    "path": "service/article_type_service.py",
    "chars": 4064,
    "preview": "# coding=utf-8\nimport logging\nfrom sqlalchemy.orm import contains_eager, joinedload\nfrom model.models import ArticleType"
  },
  {
    "path": "service/blog_view_service.py",
    "chars": 859,
    "preview": "# coding=utf-8\nimport logging\nimport datetime\nfrom model.models import BlogView\n\nlogger = logging.getLogger(__name__)\n\n\n"
  },
  {
    "path": "service/comment_service.py",
    "chars": 3551,
    "preview": "# coding=utf-8\nimport logging\n\nfrom model.models import Comment\nfrom sqlalchemy.sql import func\nfrom sqlalchemy.orm impo"
  },
  {
    "path": "service/custom_service.py",
    "chars": 864,
    "preview": "# coding=utf-8\nfrom model.models import BlogInfo\n\n\"\"\"\n博客定制相关服务\n\"\"\"\n\n\nclass BlogInfoService(object):\n\n    @staticmethod\n "
  },
  {
    "path": "service/init_service.py",
    "chars": 20122,
    "preview": "# coding=utf-8\nimport json\nimport logging\n\nimport tornado.gen\n\nfrom article_service import ArticleService\nfrom article_t"
  },
  {
    "path": "service/menu_service.py",
    "chars": 3322,
    "preview": "# coding=utf-8\nimport logging\n\nfrom sqlalchemy import func\n\nfrom article_type_service import ArticleTypeService\nfrom mod"
  },
  {
    "path": "service/plugin_service.py",
    "chars": 3696,
    "preview": "# coding=utf-8\nimport logging\nfrom sqlalchemy import func\nfrom model.models import Plugin\nfrom model.search_params.plugi"
  },
  {
    "path": "service/pubsub_service.py",
    "chars": 1392,
    "preview": "# coding=utf-8\nimport logging\nimport tornado.gen\nfrom extends.pub_sub_tornadis import PubSubTornadis\nfrom init_service i"
  },
  {
    "path": "service/user_service.py",
    "chars": 1391,
    "preview": "# coding=utf-8\nfrom model.models import User\n\n\nclass UserService(object):\n\n    @staticmethod\n    def get_user(db_session"
  },
  {
    "path": "static/css/bootstrap-theme.css",
    "chars": 26132,
    "preview": "/*!\n * Bootstrap v3.3.5 (http://getbootstrap.com)\n * Copyright 2011-2015 Twitter, Inc.\n * Licensed under MIT (https://gi"
  },
  {
    "path": "static/css/bootstrap.css",
    "chars": 147430,
    "preview": "/*!\n * Bootstrap v3.3.5 (http://getbootstrap.com)\n * Copyright 2011-2015 Twitter, Inc.\n * Licensed under MIT (https://gi"
  },
  {
    "path": "static/css/common.css",
    "chars": 5838,
    "preview": "/*CSS For common appearance*/\nbody {\n    /*background: #F0F0F0;*/\n    background: url('../images/background.jpg');\n}\n#fi"
  },
  {
    "path": "static/css/prism.css",
    "chars": 3012,
    "preview": "/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+abap+actionscript+apacheconf+apl+"
  },
  {
    "path": "static/js/admin.js",
    "chars": 5570,
    "preview": "//JS For manage-articles when select articles or select comments\n$(document).ready(function () {\n    $('#select-all').cl"
  },
  {
    "path": "static/js/articleDetail.js",
    "chars": 1586,
    "preview": "function update_disable(url) {\n    var _xsrf = getCookie(\"_xsrf\");\n    $.post(url, {_xsrf:_xsrf}, function (data) {\n    "
  },
  {
    "path": "static/js/bootstrap.js",
    "chars": 68890,
    "preview": "/*!\n * Bootstrap v3.3.5 (http://getbootstrap.com)\n * Copyright 2011-2015 Twitter, Inc.\n * Licensed under the MIT license"
  },
  {
    "path": "static/js/floatButton.js",
    "chars": 1291,
    "preview": "//JS For FloatButton to goTop, goBottom and refresh\nfunction getCookie(name) {\n    var r = document.cookie.match(\"\\\\b\" +"
  },
  {
    "path": "static/js/markdown/bootstrap-markdown.js",
    "chars": 47471,
    "preview": "/* ===================================================\n * bootstrap-markdown.js v2.10.0\n * http://github.com/toopay/boot"
  },
  {
    "path": "static/js/markdown/locale/bootstrap-markdown.zh.js",
    "chars": 767,
    "preview": "/**\n * Chinese translation for bootstrap-markdown\n * benhaile <denghaier@163.com>\n */\n(function ($) {\n  $.fn.markdown.me"
  },
  {
    "path": "static/js/markdown/markdown.js",
    "chars": 51320,
    "preview": "// Released under MIT license\n// Copyright (c) 2009-2010 Dominic Baggott\n// Copyright (c) 2009-2010 Ash Berlin\n// Copyri"
  },
  {
    "path": "static/js/markdown/to-markdown.js",
    "chars": 5938,
    "preview": "/*\n * to-markdown - an HTML to Markdown converter\n *\n * Copyright 2011, Dom Christie\n * Licenced under the MIT licence\n "
  },
  {
    "path": "static/js/markdownEdit.js",
    "chars": 561,
    "preview": "/**\n * Created by mhq on 17/1/8.\n */\nvar markdown_reg = /[\\\\\\`\\*\\_\\[\\]\\#\\+\\-\\!\\>\\s]/g;\n$(function () {\n    $('.markdown-"
  },
  {
    "path": "static/js/npm.js",
    "chars": 484,
    "preview": "// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.\nrequ"
  },
  {
    "path": "static/js/super.js",
    "chars": 346,
    "preview": "function checkPasswordForm() {\n    var password = $('#password').val();\n    var password2 = $('#password2').val();\n    i"
  },
  {
    "path": "static/js/tinymce_setup.js",
    "chars": 1358,
    "preview": "//For submit articles\ntinymce.init({\n    selector: '#content',\n    directionality:'ltr',\n    language:'zh_CN',\n    heigh"
  },
  {
    "path": "static/tinymce/LICENSE.TXT",
    "chars": 26427,
    "preview": "\t\t  GNU LESSER GENERAL PUBLIC LICENSE\n\t\t       Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Found"
  },
  {
    "path": "static/tinymce/changelog.txt",
    "chars": 90915,
    "preview": "Version 4.3.4 (2016-02-11)\n\tAdded new OpenWindow/CloseWindow events that gets fired when windows open/close.\n\tAdded new "
  },
  {
    "path": "static/tinymce/js/tinymce/extentsion_self/codesimple_extentsion/prism.js",
    "chars": 210718,
    "preview": "/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+abap+actionscript+apacheconf+apl+"
  },
  {
    "path": "static/tinymce/js/tinymce/langs/readme.md",
    "chars": 151,
    "preview": "This is where language files should be placed.\n\nPlease DO NOT translate these directly use this service: https://www.tra"
  },
  {
    "path": "static/tinymce/js/tinymce/langs/zh_CN.js",
    "chars": 8482,
    "preview": "tinymce.addI18n('zh_CN',{\n\"Cut\": \"\\u526a\\u5207\",\n\"Heading 5\": \"\\u6807\\u98985\",\n\"Header 2\": \"\\u6807\\u98982\",\n\"Your browse"
  },
  {
    "path": "static/tinymce/js/tinymce/license.txt",
    "chars": 26427,
    "preview": "\t\t  GNU LESSER GENERAL PUBLIC LICENSE\n\t\t       Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Found"
  },
  {
    "path": "static/tinymce/js/tinymce/plugins/codesample/css/prism.css",
    "chars": 2289,
    "preview": "/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript */\n/**\n * prism.js default theme "
  },
  {
    "path": "static/tinymce/js/tinymce/plugins/example/dialog.html",
    "chars": 213,
    "preview": "<!DOCTYPE html>\n<html>\n<body>\n\t<h3>Custom dialog</h3>\n\tInput some text: <input id=\"content\">\n\t<button onclick=\"top.tinym"
  },
  {
    "path": "static/tinymce/js/tinymce/plugins/visualblocks/css/visualblocks.css",
    "chars": 5070,
    "preview": ".mce-visualblocks p {\n\tpadding-top: 10px;\n\tborder: 1px dashed #BBB;\n\tmargin-left: 3px;\n\tbackground: transparent no-repea"
  },
  {
    "path": "static/tinymce/js/tinymce/skins/myskin/Variables.less",
    "chars": 7272,
    "preview": "// Variables\n// Syntax: <control>-(<sub control>)-<bg|border|text>-(<state>)-(<extra>);\n// Example: @btn-primary-bg-hove"
  },
  {
    "path": "static/tinymce/js/tinymce/skins/myskin/fonts/readme.md",
    "chars": 67,
    "preview": "Icons are generated and provided by the http://icomoon.io service.\n"
  },
  {
    "path": "static/tinymce/js/tinymce/skins/myskin/fonts/tinymce-small.json",
    "chars": 40273,
    "preview": "{\n\t\"IcoMoonType\": \"selection\",\n\t\"icons\": [\n\t\t{\n\t\t\t\"icon\": {\n\t\t\t\t\"paths\": [\n\t\t\t\t\t\"M704 832v-37.004c151.348-61.628 256-193"
  },
  {
    "path": "static/tinymce/js/tinymce/skins/myskin/fonts/tinymce.json",
    "chars": 58313,
    "preview": "{\n\t\"selection\": [\n\t\t{\n\t\t\t\"order\": 0,\n\t\t\t\"id\": 0,\n\t\t\t\"prevSize\": 32,\n\t\t\t\"code\": 58882,\n\t\t\t\"name\": \"invert\",\n\t\t\t\"tempChar\""
  },
  {
    "path": "static/tinymce/js/tinymce/skins/myskin/skin.json",
    "chars": 2583,
    "preview": "{\r\n\t\"skin-name\": \"myskin\",\r\n\t\"preview-bg\": \"#666666\",\r\n\t\"text\": \"#b5b9bf\",\r\n\t\"text-inverse\": \"#000000\",\r\n\t\"text-disabled"
  },
  {
    "path": "template/403.html",
    "chars": 153,
    "preview": "{% extends 'base.html' %}\n\n{% block title %}\n    禁止访问\n{% end %}\n{% block content %}\n<div class=\"page-header\">\n    <h1>亲,"
  },
  {
    "path": "template/404.html",
    "chars": 158,
    "preview": "{% extends 'base.html' %}\n\n{% block title %}\n    无法找到该页面\n{% end %}\n{% block content %}\n<div class=\"page-header\">\n    <h1"
  },
  {
    "path": "template/500.html",
    "chars": 157,
    "preview": "{% extends 'base.html' %}\n\n{% block title %}\n    服务器内部错误\n{% end %}\n{% block content %}\n<div class=\"page-header\">\n    <h1"
  },
  {
    "path": "template/_article_comments.html",
    "chars": 4853,
    "preview": "<ul class=\"comments\">\n    {% if comments_pager and comments_pager.result %}\n        {% for comment in comments_pager.res"
  },
  {
    "path": "template/_macros.html",
    "chars": 2591,
    "preview": "<div class=\"pagination\">\n    <ul class=\"pagination\">\n        <li {% if not pager.has_prev() %} class=\"disabled\" {% end %"
  },
  {
    "path": "template/admin/admin_account.html",
    "chars": 4535,
    "preview": "{% extends 'admin_base.html' %}\n\n{% block admin_content %}\n<div class=\"entry-box account\">\n    <h4><strong><i>{{ current"
  },
  {
    "path": "template/admin/admin_base.html",
    "chars": 1804,
    "preview": "{% extends '../base.html' %}\n\n{% block title %}\n    blog_xtg - {% block title2 %}欢迎来到blog_xtg管理平台!{% end %}\n{% end %}\n\n{"
  },
  {
    "path": "template/admin/blog_plugin_add.html",
    "chars": 1117,
    "preview": "{% extends 'admin_base.html' %}\n\n{% block title2 %}\n    添加插件\n{% end %}\n\n{% block admin_content %}\n<div class=\"entry-box "
  },
  {
    "path": "template/admin/blog_plugin_edit.html",
    "chars": 1172,
    "preview": "{% extends 'admin_base.html' %}\n\n{% block title2 %}\n    修改插件\n{% end %}\n\n{% block admin_content %}\n<div class=\"entry-box "
  },
  {
    "path": "template/admin/custom_blog_info.html",
    "chars": 3264,
    "preview": "{% from model.site_info import SiteCollection %}\n{% extends 'admin_base.html' %}\n\n{% block title2 %}\n    基本信息\n{% end %}\n"
  },
  {
    "path": "template/admin/custom_blog_plugin.html",
    "chars": 5087,
    "preview": "{% extends 'admin_base.html' %}\n\n{% block title2 %}\n    插件管理\n{% end %}\n\n{% block admin_content %}\n<div class=\"entry-box "
  },
  {
    "path": "template/admin/help_page.html",
    "chars": 1767,
    "preview": "{% extends 'admin_base.html' %}\n\n{% block title2 %}\n帮助\n{% end %}\n\n{% block admin_content %}\n<div class=\"entry-box\">\n    "
  },
  {
    "path": "template/admin/manage_articleTypes.html",
    "chars": 9379,
    "preview": "{% extends 'admin_base.html' %}\n\n{% block title2 %}\n    博文分类\n{% end %}\n\n{% block admin_content %}\n<div class=\"entry-box "
  },
  {
    "path": "template/admin/manage_articleTypes_nav.html",
    "chars": 7577,
    "preview": "{% extends 'admin_base.html' %}\n\n{% block title2 %}\n    分类导航\n{% end %}\n\n{% block admin_content %}\n<div class=\"entry-box "
  },
  {
    "path": "template/admin/manage_articles.html",
    "chars": 5462,
    "preview": "{% extends 'admin_base.html' %}\n\n{% block title2 %}\n    管理博文\n{% end %}\n\n{% block admin_content %}\n<div class=\"entry-box "
  },
  {
    "path": "template/admin/manage_comments.html",
    "chars": 7045,
    "preview": "{% extends 'admin_base.html' %}\n\n{% block title2 %}\n    博文评论\n{% end %}\n\n{% block admin_content %}\n<div class=\"entry-box "
  },
  {
    "path": "template/admin/submit_articles.html",
    "chars": 2750,
    "preview": "{% extends 'admin_base.html' %}\n\n{% block title2 %}\n    发表博文\n{% end %}\n\n{% block private_stylesheet %}\n    <link rel=\"st"
  },
  {
    "path": "template/article_detials.html",
    "chars": 3769,
    "preview": "{% extends 'base.html' %}\n\n{% block title %}\n    {{ article.title }}\n{% end %}\n{% block private_stylesheet %}\n    <link "
  },
  {
    "path": "template/auth/login.html",
    "chars": 2649,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>登陆blog_mhq管理后台</title>\n    <link href=\"{{ "
  },
  {
    "path": "template/base.html",
    "chars": 8421,
    "preview": "<!DOCTYPE html>\n{% from model.site_info import SiteCollection %}\n{% from model.constants import Constants %}\n<html>\n<hea"
  },
  {
    "path": "template/index.html",
    "chars": 1538,
    "preview": "{% extends 'base.html' %}\n\n{% block content %}\n\n{% if pager and pager.result %}\n{% for article in pager.result %}\n<div i"
  },
  {
    "path": "template/super/init.html",
    "chars": 2465,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>创建管理员账户</title>\n    <link href=\"{{ static_"
  },
  {
    "path": "url_mapping.py",
    "chars": 3530,
    "preview": "# coding=utf-8\nimport controller.home\nimport controller.admin\nimport controller.admin_custom\nimport controller.admin_art"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the xtg20121013/blog_xtg GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 107 files (1.0 MB), approximately 327.9k tokens, and a symbol index with 363 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!