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 #时区问题(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 '' % 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 '' % 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 '' % 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 '' % 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 '
' % 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 '' % 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-console:before { content: "\e254"; } .glyphicon-superscript:before { content: "\e255"; } .glyphicon-subscript:before { content: "\e256"; } .glyphicon-menu-left:before { content: "\e257"; } .glyphicon-menu-right:before { content: "\e258"; } .glyphicon-menu-down:before { content: "\e259"; } .glyphicon-menu-up:before { content: "\e260"; } * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } *:before, *:after { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } html { font-size: 10px; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.42857143; color: #333; background-color: #fff; } input, button, select, textarea { font-family: inherit; font-size: inherit; line-height: inherit; } a { color: #337ab7; text-decoration: none; } a:hover, a:focus { color: #23527c; text-decoration: underline; } a:focus { outline: thin dotted; outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } figure { margin: 0; } img { vertical-align: middle; } .img-responsive, .thumbnail > img, .thumbnail a > img, .carousel-inner > .item > img, .carousel-inner > .item > a > img { display: block; max-width: 100%; height: auto; } .img-rounded { border-radius: 6px; } .img-thumbnail { display: inline-block; max-width: 100%; height: auto; padding: 4px; line-height: 1.42857143; background-color: #fff; border: 1px solid #ddd; border-radius: 4px; -webkit-transition: all .2s ease-in-out; -o-transition: all .2s ease-in-out; transition: all .2s ease-in-out; } .img-circle { border-radius: 50%; } hr { margin-top: 20px; margin-bottom: 20px; border: 0; border-top: 1px solid #eee; } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; } .sr-only-focusable:active, .sr-only-focusable:focus { position: static; width: auto; height: auto; margin: 0; overflow: visible; clip: auto; } [role="button"] { cursor: pointer; } h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { font-family: inherit; font-weight: 500; line-height: 1.1; color: inherit; } h1 small, h2 small, h3 small, h4 small, h5 small, h6 small, .h1 small, .h2 small, .h3 small, .h4 small, .h5 small, .h6 small, h1 .small, h2 .small, h3 .small, h4 .small, h5 .small, h6 .small, .h1 .small, .h2 .small, .h3 .small, .h4 .small, .h5 .small, .h6 .small { font-weight: normal; line-height: 1; color: #777; } h1, .h1, h2, .h2, h3, .h3 { margin-top: 20px; margin-bottom: 10px; } h1 small, .h1 small, h2 small, .h2 small, h3 small, .h3 small, h1 .small, .h1 .small, h2 .small, .h2 .small, h3 .small, .h3 .small { font-size: 65%; } h4, .h4, h5, .h5, h6, .h6 { margin-top: 10px; margin-bottom: 10px; } h4 small, .h4 small, h5 small, .h5 small, h6 small, .h6 small, h4 .small, .h4 .small, h5 .small, .h5 .small, h6 .small, .h6 .small { font-size: 75%; } h1, .h1 { font-size: 36px; } h2, .h2 { font-size: 30px; } h3, .h3 { font-size: 24px; } h4, .h4 { font-size: 18px; } h5, .h5 { font-size: 14px; } h6, .h6 { font-size: 12px; } p { margin: 0 0 10px; } .lead { margin-bottom: 20px; font-size: 16px; font-weight: 300; line-height: 1.4; } @media (min-width: 768px) { .lead { font-size: 21px; } } small, .small { font-size: 85%; } mark, .mark { padding: .2em; background-color: #fcf8e3; } .text-left { text-align: left; } .text-right { text-align: right; } .text-center { text-align: center; } .text-justify { text-align: justify; } .text-nowrap { white-space: nowrap; } .text-lowercase { text-transform: lowercase; } .text-uppercase { text-transform: uppercase; } .text-capitalize { text-transform: capitalize; } .text-muted { color: #777; } .text-primary { color: #337ab7; } a.text-primary:hover, a.text-primary:focus { color: #286090; } .text-success { color: #3c763d; } a.text-success:hover, a.text-success:focus { color: #2b542c; } .text-info { color: #31708f; } a.text-info:hover, a.text-info:focus { color: #245269; } .text-warning { color: #8a6d3b; } a.text-warning:hover, a.text-warning:focus { color: #66512c; } .text-danger { color: #a94442; } a.text-danger:hover, a.text-danger:focus { color: #843534; } .bg-primary { color: #fff; background-color: #337ab7; } a.bg-primary:hover, a.bg-primary:focus { background-color: #286090; } .bg-success { background-color: #dff0d8; } a.bg-success:hover, a.bg-success:focus { background-color: #c1e2b3; } .bg-info { background-color: #d9edf7; } a.bg-info:hover, a.bg-info:focus { background-color: #afd9ee; } .bg-warning { background-color: #fcf8e3; } a.bg-warning:hover, a.bg-warning:focus { background-color: #f7ecb5; } .bg-danger { background-color: #f2dede; } a.bg-danger:hover, a.bg-danger:focus { background-color: #e4b9b9; } .page-header { padding-bottom: 9px; margin: 40px 0 20px; border-bottom: 1px solid #eee; } ul, ol { margin-top: 0; margin-bottom: 10px; } ul ul, ol ul, ul ol, ol ol { margin-bottom: 0; } .list-unstyled { padding-left: 0; list-style: none; } .list-inline { padding-left: 0; margin-left: -5px; list-style: none; } .list-inline > li { display: inline-block; padding-right: 5px; padding-left: 5px; } dl { margin-top: 0; margin-bottom: 20px; } dt, dd { line-height: 1.42857143; } dt { font-weight: bold; } dd { margin-left: 0; } @media (min-width: 768px) { .dl-horizontal dt { float: left; width: 160px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } .dl-horizontal dd { margin-left: 180px; } } abbr[title], abbr[data-original-title] { cursor: help; border-bottom: 1px dotted #777; } .initialism { font-size: 90%; text-transform: uppercase; } blockquote { padding: 10px 20px; margin: 0 0 20px; font-size: 17.5px; border-left: 5px solid #eee; } blockquote p:last-child, blockquote ul:last-child, blockquote ol:last-child { margin-bottom: 0; } blockquote footer, blockquote small, blockquote .small { display: block; font-size: 80%; line-height: 1.42857143; color: #777; } blockquote footer:before, blockquote small:before, blockquote .small:before { content: '\2014 \00A0'; } .blockquote-reverse, blockquote.pull-right { padding-right: 15px; padding-left: 0; text-align: right; border-right: 5px solid #eee; border-left: 0; } .blockquote-reverse footer:before, blockquote.pull-right footer:before, .blockquote-reverse small:before, blockquote.pull-right small:before, .blockquote-reverse .small:before, blockquote.pull-right .small:before { content: ''; } .blockquote-reverse footer:after, blockquote.pull-right footer:after, .blockquote-reverse small:after, blockquote.pull-right small:after, .blockquote-reverse .small:after, blockquote.pull-right .small:after { content: '\00A0 \2014'; } address { margin-bottom: 20px; font-style: normal; line-height: 1.42857143; } code, kbd, pre, samp { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; } code { padding: 2px 4px; font-size: 90%; color: #c7254e; background-color: #f9f2f4; border-radius: 4px; } kbd { padding: 2px 4px; font-size: 90%; color: #fff; background-color: #333; border-radius: 3px; -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); } kbd kbd { padding: 0; font-size: 100%; font-weight: bold; -webkit-box-shadow: none; box-shadow: none; } pre { display: block; padding: 9.5px; margin: 0 0 10px; font-size: 13px; line-height: 1.42857143; color: #333; word-break: break-all; word-wrap: break-word; background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; } pre code { padding: 0; font-size: inherit; color: inherit; white-space: pre-wrap; background-color: transparent; border-radius: 0; } .pre-scrollable { max-height: 340px; overflow-y: scroll; } .container { padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } @media (min-width: 768px) { .container { width: 750px; } } @media (min-width: 992px) { .container { width: 970px; } } @media (min-width: 1200px) { .container { width: 1170px; } } .container-fluid { padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } .row { margin-right: -15px; margin-left: -15px; } .col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { position: relative; min-height: 1px; padding-right: 15px; padding-left: 15px; } .col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { float: left; } .col-xs-12 { width: 100%; } .col-xs-11 { width: 91.66666667%; } .col-xs-10 { width: 83.33333333%; } .col-xs-9 { width: 75%; } .col-xs-8 { width: 66.66666667%; } .col-xs-7 { width: 58.33333333%; } .col-xs-6 { width: 50%; } .col-xs-5 { width: 41.66666667%; } .col-xs-4 { width: 33.33333333%; } .col-xs-3 { width: 25%; } .col-xs-2 { width: 16.66666667%; } .col-xs-1 { width: 8.33333333%; } .col-xs-pull-12 { right: 100%; } .col-xs-pull-11 { right: 91.66666667%; } .col-xs-pull-10 { right: 83.33333333%; } .col-xs-pull-9 { right: 75%; } .col-xs-pull-8 { right: 66.66666667%; } .col-xs-pull-7 { right: 58.33333333%; } .col-xs-pull-6 { right: 50%; } .col-xs-pull-5 { right: 41.66666667%; } .col-xs-pull-4 { right: 33.33333333%; } .col-xs-pull-3 { right: 25%; } .col-xs-pull-2 { right: 16.66666667%; } .col-xs-pull-1 { right: 8.33333333%; } .col-xs-pull-0 { right: auto; } .col-xs-push-12 { left: 100%; } .col-xs-push-11 { left: 91.66666667%; } .col-xs-push-10 { left: 83.33333333%; } .col-xs-push-9 { left: 75%; } .col-xs-push-8 { left: 66.66666667%; } .col-xs-push-7 { left: 58.33333333%; } .col-xs-push-6 { left: 50%; } .col-xs-push-5 { left: 41.66666667%; } .col-xs-push-4 { left: 33.33333333%; } .col-xs-push-3 { left: 25%; } .col-xs-push-2 { left: 16.66666667%; } .col-xs-push-1 { left: 8.33333333%; } .col-xs-push-0 { left: auto; } .col-xs-offset-12 { margin-left: 100%; } .col-xs-offset-11 { margin-left: 91.66666667%; } .col-xs-offset-10 { margin-left: 83.33333333%; } .col-xs-offset-9 { margin-left: 75%; } .col-xs-offset-8 { margin-left: 66.66666667%; } .col-xs-offset-7 { margin-left: 58.33333333%; } .col-xs-offset-6 { margin-left: 50%; } .col-xs-offset-5 { margin-left: 41.66666667%; } .col-xs-offset-4 { margin-left: 33.33333333%; } .col-xs-offset-3 { margin-left: 25%; } .col-xs-offset-2 { margin-left: 16.66666667%; } .col-xs-offset-1 { margin-left: 8.33333333%; } .col-xs-offset-0 { margin-left: 0; } @media (min-width: 768px) { .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { float: left; } .col-sm-12 { width: 100%; } .col-sm-11 { width: 91.66666667%; } .col-sm-10 { width: 83.33333333%; } .col-sm-9 { width: 75%; } .col-sm-8 { width: 66.66666667%; } .col-sm-7 { width: 58.33333333%; } .col-sm-6 { width: 50%; } .col-sm-5 { width: 41.66666667%; } .col-sm-4 { width: 33.33333333%; } .col-sm-3 { width: 25%; } .col-sm-2 { width: 16.66666667%; } .col-sm-1 { width: 8.33333333%; } .col-sm-pull-12 { right: 100%; } .col-sm-pull-11 { right: 91.66666667%; } .col-sm-pull-10 { right: 83.33333333%; } .col-sm-pull-9 { right: 75%; } .col-sm-pull-8 { right: 66.66666667%; } .col-sm-pull-7 { right: 58.33333333%; } .col-sm-pull-6 { right: 50%; } .col-sm-pull-5 { right: 41.66666667%; } .col-sm-pull-4 { right: 33.33333333%; } .col-sm-pull-3 { right: 25%; } .col-sm-pull-2 { right: 16.66666667%; } .col-sm-pull-1 { right: 8.33333333%; } .col-sm-pull-0 { right: auto; } .col-sm-push-12 { left: 100%; } .col-sm-push-11 { left: 91.66666667%; } .col-sm-push-10 { left: 83.33333333%; } .col-sm-push-9 { left: 75%; } .col-sm-push-8 { left: 66.66666667%; } .col-sm-push-7 { left: 58.33333333%; } .col-sm-push-6 { left: 50%; } .col-sm-push-5 { left: 41.66666667%; } .col-sm-push-4 { left: 33.33333333%; } .col-sm-push-3 { left: 25%; } .col-sm-push-2 { left: 16.66666667%; } .col-sm-push-1 { left: 8.33333333%; } .col-sm-push-0 { left: auto; } .col-sm-offset-12 { margin-left: 100%; } .col-sm-offset-11 { margin-left: 91.66666667%; } .col-sm-offset-10 { margin-left: 83.33333333%; } .col-sm-offset-9 { margin-left: 75%; } .col-sm-offset-8 { margin-left: 66.66666667%; } .col-sm-offset-7 { margin-left: 58.33333333%; } .col-sm-offset-6 { margin-left: 50%; } .col-sm-offset-5 { margin-left: 41.66666667%; } .col-sm-offset-4 { margin-left: 33.33333333%; } .col-sm-offset-3 { margin-left: 25%; } .col-sm-offset-2 { margin-left: 16.66666667%; } .col-sm-offset-1 { margin-left: 8.33333333%; } .col-sm-offset-0 { margin-left: 0; } } @media (min-width: 992px) { .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { float: left; } .col-md-12 { width: 100%; } .col-md-11 { width: 91.66666667%; } .col-md-10 { width: 83.33333333%; } .col-md-9 { width: 75%; } .col-md-8 { width: 66.66666667%; } .col-md-7 { width: 58.33333333%; } .col-md-6 { width: 50%; } .col-md-5 { width: 41.66666667%; } .col-md-4 { width: 33.33333333%; } .col-md-3 { width: 25%; } .col-md-2 { width: 16.66666667%; } .col-md-1 { width: 8.33333333%; } .col-md-pull-12 { right: 100%; } .col-md-pull-11 { right: 91.66666667%; } .col-md-pull-10 { right: 83.33333333%; } .col-md-pull-9 { right: 75%; } .col-md-pull-8 { right: 66.66666667%; } .col-md-pull-7 { right: 58.33333333%; } .col-md-pull-6 { right: 50%; } .col-md-pull-5 { right: 41.66666667%; } .col-md-pull-4 { right: 33.33333333%; } .col-md-pull-3 { right: 25%; } .col-md-pull-2 { right: 16.66666667%; } .col-md-pull-1 { right: 8.33333333%; } .col-md-pull-0 { right: auto; } .col-md-push-12 { left: 100%; } .col-md-push-11 { left: 91.66666667%; } .col-md-push-10 { left: 83.33333333%; } .col-md-push-9 { left: 75%; } .col-md-push-8 { left: 66.66666667%; } .col-md-push-7 { left: 58.33333333%; } .col-md-push-6 { left: 50%; } .col-md-push-5 { left: 41.66666667%; } .col-md-push-4 { left: 33.33333333%; } .col-md-push-3 { left: 25%; } .col-md-push-2 { left: 16.66666667%; } .col-md-push-1 { left: 8.33333333%; } .col-md-push-0 { left: auto; } .col-md-offset-12 { margin-left: 100%; } .col-md-offset-11 { margin-left: 91.66666667%; } .col-md-offset-10 { margin-left: 83.33333333%; } .col-md-offset-9 { margin-left: 75%; } .col-md-offset-8 { margin-left: 66.66666667%; } .col-md-offset-7 { margin-left: 58.33333333%; } .col-md-offset-6 { margin-left: 50%; } .col-md-offset-5 { margin-left: 41.66666667%; } .col-md-offset-4 { margin-left: 33.33333333%; } .col-md-offset-3 { margin-left: 25%; } .col-md-offset-2 { margin-left: 16.66666667%; } .col-md-offset-1 { margin-left: 8.33333333%; } .col-md-offset-0 { margin-left: 0; } } @media (min-width: 1200px) { .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { float: left; } .col-lg-12 { width: 100%; } .col-lg-11 { width: 91.66666667%; } .col-lg-10 { width: 83.33333333%; } .col-lg-9 { width: 75%; } .col-lg-8 { width: 66.66666667%; } .col-lg-7 { width: 58.33333333%; } .col-lg-6 { width: 50%; } .col-lg-5 { width: 41.66666667%; } .col-lg-4 { width: 33.33333333%; } .col-lg-3 { width: 25%; } .col-lg-2 { width: 16.66666667%; } .col-lg-1 { width: 8.33333333%; } .col-lg-pull-12 { right: 100%; } .col-lg-pull-11 { right: 91.66666667%; } .col-lg-pull-10 { right: 83.33333333%; } .col-lg-pull-9 { right: 75%; } .col-lg-pull-8 { right: 66.66666667%; } .col-lg-pull-7 { right: 58.33333333%; } .col-lg-pull-6 { right: 50%; } .col-lg-pull-5 { right: 41.66666667%; } .col-lg-pull-4 { right: 33.33333333%; } .col-lg-pull-3 { right: 25%; } .col-lg-pull-2 { right: 16.66666667%; } .col-lg-pull-1 { right: 8.33333333%; } .col-lg-pull-0 { right: auto; } .col-lg-push-12 { left: 100%; } .col-lg-push-11 { left: 91.66666667%; } .col-lg-push-10 { left: 83.33333333%; } .col-lg-push-9 { left: 75%; } .col-lg-push-8 { left: 66.66666667%; } .col-lg-push-7 { left: 58.33333333%; } .col-lg-push-6 { left: 50%; } .col-lg-push-5 { left: 41.66666667%; } .col-lg-push-4 { left: 33.33333333%; } .col-lg-push-3 { left: 25%; } .col-lg-push-2 { left: 16.66666667%; } .col-lg-push-1 { left: 8.33333333%; } .col-lg-push-0 { left: auto; } .col-lg-offset-12 { margin-left: 100%; } .col-lg-offset-11 { margin-left: 91.66666667%; } .col-lg-offset-10 { margin-left: 83.33333333%; } .col-lg-offset-9 { margin-left: 75%; } .col-lg-offset-8 { margin-left: 66.66666667%; } .col-lg-offset-7 { margin-left: 58.33333333%; } .col-lg-offset-6 { margin-left: 50%; } .col-lg-offset-5 { margin-left: 41.66666667%; } .col-lg-offset-4 { margin-left: 33.33333333%; } .col-lg-offset-3 { margin-left: 25%; } .col-lg-offset-2 { margin-left: 16.66666667%; } .col-lg-offset-1 { margin-left: 8.33333333%; } .col-lg-offset-0 { margin-left: 0; } } table { background-color: transparent; } caption { padding-top: 8px; padding-bottom: 8px; color: #777; text-align: left; } th { text-align: left; } .table { width: 100%; max-width: 100%; margin-bottom: 20px; } .table > thead > tr > th, .table > tbody > tr > th, .table > tfoot > tr > th, .table > thead > tr > td, .table > tbody > tr > td, .table > tfoot > tr > td { padding: 8px; line-height: 1.42857143; vertical-align: top; border-top: 1px solid #ddd; } .table > thead > tr > th { vertical-align: bottom; border-bottom: 2px solid #ddd; } .table > caption + thead > tr:first-child > th, .table > colgroup + thead > tr:first-child > th, .table > thead:first-child > tr:first-child > th, .table > caption + thead > tr:first-child > td, .table > colgroup + thead > tr:first-child > td, .table > thead:first-child > tr:first-child > td { border-top: 0; } .table > tbody + tbody { border-top: 2px solid #ddd; } .table .table { background-color: #fff; } .table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td { padding: 5px; } .table-bordered { border: 1px solid #ddd; } .table-bordered > thead > tr > th, .table-bordered > tbody > tr > th, .table-bordered > tfoot > tr > th, .table-bordered > thead > tr > td, .table-bordered > tbody > tr > td, .table-bordered > tfoot > tr > td { border: 1px solid #ddd; } .table-bordered > thead > tr > th, .table-bordered > thead > tr > td { border-bottom-width: 2px; } .table-striped > tbody > tr:nth-of-type(odd) { background-color: #f9f9f9; } .table-hover > tbody > tr:hover { background-color: #f5f5f5; } table col[class*="col-"] { position: static; display: table-column; float: none; } table td[class*="col-"], table th[class*="col-"] { position: static; display: table-cell; float: none; } .table > thead > tr > td.active, .table > tbody > tr > td.active, .table > tfoot > tr > td.active, .table > thead > tr > th.active, .table > tbody > tr > th.active, .table > tfoot > tr > th.active, .table > thead > tr.active > td, .table > tbody > tr.active > td, .table > tfoot > tr.active > td, .table > thead > tr.active > th, .table > tbody > tr.active > th, .table > tfoot > tr.active > th { background-color: #f5f5f5; } .table-hover > tbody > tr > td.active:hover, .table-hover > tbody > tr > th.active:hover, .table-hover > tbody > tr.active:hover > td, .table-hover > tbody > tr:hover > .active, .table-hover > tbody > tr.active:hover > th { background-color: #e8e8e8; } .table > thead > tr > td.success, .table > tbody > tr > td.success, .table > tfoot > tr > td.success, .table > thead > tr > th.success, .table > tbody > tr > th.success, .table > tfoot > tr > th.success, .table > thead > tr.success > td, .table > tbody > tr.success > td, .table > tfoot > tr.success > td, .table > thead > tr.success > th, .table > tbody > tr.success > th, .table > tfoot > tr.success > th { background-color: #dff0d8; } .table-hover > tbody > tr > td.success:hover, .table-hover > tbody > tr > th.success:hover, .table-hover > tbody > tr.success:hover > td, .table-hover > tbody > tr:hover > .success, .table-hover > tbody > tr.success:hover > th { background-color: #d0e9c6; } .table > thead > tr > td.info, .table > tbody > tr > td.info, .table > tfoot > tr > td.info, .table > thead > tr > th.info, .table > tbody > tr > th.info, .table > tfoot > tr > th.info, .table > thead > tr.info > td, .table > tbody > tr.info > td, .table > tfoot > tr.info > td, .table > thead > tr.info > th, .table > tbody > tr.info > th, .table > tfoot > tr.info > th { background-color: #d9edf7; } .table-hover > tbody > tr > td.info:hover, .table-hover > tbody > tr > th.info:hover, .table-hover > tbody > tr.info:hover > td, .table-hover > tbody > tr:hover > .info, .table-hover > tbody > tr.info:hover > th { background-color: #c4e3f3; } .table > thead > tr > td.warning, .table > tbody > tr > td.warning, .table > tfoot > tr > td.warning, .table > thead > tr > th.warning, .table > tbody > tr > th.warning, .table > tfoot > tr > th.warning, .table > thead > tr.warning > td, .table > tbody > tr.warning > td, .table > tfoot > tr.warning > td, .table > thead > tr.warning > th, .table > tbody > tr.warning > th, .table > tfoot > tr.warning > th { background-color: #fcf8e3; } .table-hover > tbody > tr > td.warning:hover, .table-hover > tbody > tr > th.warning:hover, .table-hover > tbody > tr.warning:hover > td, .table-hover > tbody > tr:hover > .warning, .table-hover > tbody > tr.warning:hover > th { background-color: #faf2cc; } .table > thead > tr > td.danger, .table > tbody > tr > td.danger, .table > tfoot > tr > td.danger, .table > thead > tr > th.danger, .table > tbody > tr > th.danger, .table > tfoot > tr > th.danger, .table > thead > tr.danger > td, .table > tbody > tr.danger > td, .table > tfoot > tr.danger > td, .table > thead > tr.danger > th, .table > tbody > tr.danger > th, .table > tfoot > tr.danger > th { background-color: #f2dede; } .table-hover > tbody > tr > td.danger:hover, .table-hover > tbody > tr > th.danger:hover, .table-hover > tbody > tr.danger:hover > td, .table-hover > tbody > tr:hover > .danger, .table-hover > tbody > tr.danger:hover > th { background-color: #ebcccc; } .table-responsive { min-height: .01%; overflow-x: auto; } @media screen and (max-width: 767px) { .table-responsive { width: 100%; margin-bottom: 15px; overflow-y: hidden; -ms-overflow-style: -ms-autohiding-scrollbar; border: 1px solid #ddd; } .table-responsive > .table { margin-bottom: 0; } .table-responsive > .table > thead > tr > th, .table-responsive > .table > tbody > tr > th, .table-responsive > .table > tfoot > tr > th, .table-responsive > .table > thead > tr > td, .table-responsive > .table > tbody > tr > td, .table-responsive > .table > tfoot > tr > td { white-space: nowrap; } .table-responsive > .table-bordered { border: 0; } .table-responsive > .table-bordered > thead > tr > th:first-child, .table-responsive > .table-bordered > tbody > tr > th:first-child, .table-responsive > .table-bordered > tfoot > tr > th:first-child, .table-responsive > .table-bordered > thead > tr > td:first-child, .table-responsive > .table-bordered > tbody > tr > td:first-child, .table-responsive > .table-bordered > tfoot > tr > td:first-child { border-left: 0; } .table-responsive > .table-bordered > thead > tr > th:last-child, .table-responsive > .table-bordered > tbody > tr > th:last-child, .table-responsive > .table-bordered > tfoot > tr > th:last-child, .table-responsive > .table-bordered > thead > tr > td:last-child, .table-responsive > .table-bordered > tbody > tr > td:last-child, .table-responsive > .table-bordered > tfoot > tr > td:last-child { border-right: 0; } .table-responsive > .table-bordered > tbody > tr:last-child > th, .table-responsive > .table-bordered > tfoot > tr:last-child > th, .table-responsive > .table-bordered > tbody > tr:last-child > td, .table-responsive > .table-bordered > tfoot > tr:last-child > td { border-bottom: 0; } } fieldset { min-width: 0; padding: 0; margin: 0; border: 0; } legend { display: block; width: 100%; padding: 0; margin-bottom: 20px; font-size: 21px; line-height: inherit; color: #333; border: 0; border-bottom: 1px solid #e5e5e5; } label { display: inline-block; max-width: 100%; margin-bottom: 5px; font-weight: bold; } input[type="search"] { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } input[type="radio"], input[type="checkbox"] { margin: 4px 0 0; margin-top: 1px \9; line-height: normal; } input[type="file"] { display: block; } input[type="range"] { display: block; width: 100%; } select[multiple], select[size] { height: auto; } input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { outline: thin dotted; outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } output { display: block; padding-top: 7px; font-size: 14px; line-height: 1.42857143; color: #555; } .form-control { display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } .form-control:focus { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); } .form-control::-moz-placeholder { color: #999; opacity: 1; } .form-control:-ms-input-placeholder { color: #999; } .form-control::-webkit-input-placeholder { color: #999; } .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { background-color: #eee; opacity: 1; } .form-control[disabled], fieldset[disabled] .form-control { cursor: not-allowed; } textarea.form-control { height: auto; } input[type="search"] { -webkit-appearance: none; } @media screen and (-webkit-min-device-pixel-ratio: 0) { input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control { line-height: 34px; } input[type="date"].input-sm, input[type="time"].input-sm, input[type="datetime-local"].input-sm, input[type="month"].input-sm, .input-group-sm input[type="date"], .input-group-sm input[type="time"], .input-group-sm input[type="datetime-local"], .input-group-sm input[type="month"] { line-height: 30px; } input[type="date"].input-lg, input[type="time"].input-lg, input[type="datetime-local"].input-lg, input[type="month"].input-lg, .input-group-lg input[type="date"], .input-group-lg input[type="time"], .input-group-lg input[type="datetime-local"], .input-group-lg input[type="month"] { line-height: 46px; } } .form-group { margin-bottom: 15px; } .radio, .checkbox { position: relative; display: block; margin-top: 10px; margin-bottom: 10px; } .radio label, .checkbox label { min-height: 20px; padding-left: 20px; margin-bottom: 0; font-weight: normal; cursor: pointer; } .radio input[type="radio"], .radio-inline input[type="radio"], .checkbox input[type="checkbox"], .checkbox-inline input[type="checkbox"] { position: absolute; margin-top: 4px \9; margin-left: -20px; } .radio + .radio, .checkbox + .checkbox { margin-top: -5px; } .radio-inline, .checkbox-inline { position: relative; display: inline-block; padding-left: 20px; margin-bottom: 0; font-weight: normal; vertical-align: middle; cursor: pointer; } .radio-inline + .radio-inline, .checkbox-inline + .checkbox-inline { margin-top: 0; margin-left: 10px; } input[type="radio"][disabled], input[type="checkbox"][disabled], input[type="radio"].disabled, input[type="checkbox"].disabled, fieldset[disabled] input[type="radio"], fieldset[disabled] input[type="checkbox"] { cursor: not-allowed; } .radio-inline.disabled, .checkbox-inline.disabled, fieldset[disabled] .radio-inline, fieldset[disabled] .checkbox-inline { cursor: not-allowed; } .radio.disabled label, .checkbox.disabled label, fieldset[disabled] .radio label, fieldset[disabled] .checkbox label { cursor: not-allowed; } .form-control-static { min-height: 34px; padding-top: 7px; padding-bottom: 7px; margin-bottom: 0; } .form-control-static.input-lg, .form-control-static.input-sm { padding-right: 0; padding-left: 0; } .input-sm { height: 30px; padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } select.input-sm { height: 30px; line-height: 30px; } textarea.input-sm, select[multiple].input-sm { height: auto; } .form-group-sm .form-control { height: 30px; padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } .form-group-sm select.form-control { height: 30px; line-height: 30px; } .form-group-sm textarea.form-control, .form-group-sm select[multiple].form-control { height: auto; } .form-group-sm .form-control-static { height: 30px; min-height: 32px; padding: 6px 10px; font-size: 12px; line-height: 1.5; } .input-lg { height: 46px; padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } select.input-lg { height: 46px; line-height: 46px; } textarea.input-lg, select[multiple].input-lg { height: auto; } .form-group-lg .form-control { height: 46px; padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } .form-group-lg select.form-control { height: 46px; line-height: 46px; } .form-group-lg textarea.form-control, .form-group-lg select[multiple].form-control { height: auto; } .form-group-lg .form-control-static { height: 46px; min-height: 38px; padding: 11px 16px; font-size: 18px; line-height: 1.3333333; } .has-feedback { position: relative; } .has-feedback .form-control { padding-right: 42.5px; } .form-control-feedback { position: absolute; top: 0; right: 0; z-index: 2; display: block; width: 34px; height: 34px; line-height: 34px; text-align: center; pointer-events: none; } .input-lg + .form-control-feedback, .input-group-lg + .form-control-feedback, .form-group-lg .form-control + .form-control-feedback { width: 46px; height: 46px; line-height: 46px; } .input-sm + .form-control-feedback, .input-group-sm + .form-control-feedback, .form-group-sm .form-control + .form-control-feedback { width: 30px; height: 30px; line-height: 30px; } .has-success .help-block, .has-success .control-label, .has-success .radio, .has-success .checkbox, .has-success .radio-inline, .has-success .checkbox-inline, .has-success.radio label, .has-success.checkbox label, .has-success.radio-inline label, .has-success.checkbox-inline label { color: #3c763d; } .has-success .form-control { border-color: #3c763d; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } .has-success .form-control:focus { border-color: #2b542c; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; } .has-success .input-group-addon { color: #3c763d; background-color: #dff0d8; border-color: #3c763d; } .has-success .form-control-feedback { color: #3c763d; } .has-warning .help-block, .has-warning .control-label, .has-warning .radio, .has-warning .checkbox, .has-warning .radio-inline, .has-warning .checkbox-inline, .has-warning.radio label, .has-warning.checkbox label, .has-warning.radio-inline label, .has-warning.checkbox-inline label { color: #8a6d3b; } .has-warning .form-control { border-color: #8a6d3b; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } .has-warning .form-control:focus { border-color: #66512c; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; } .has-warning .input-group-addon { color: #8a6d3b; background-color: #fcf8e3; border-color: #8a6d3b; } .has-warning .form-control-feedback { color: #8a6d3b; } .has-error .help-block, .has-error .control-label, .has-error .radio, .has-error .checkbox, .has-error .radio-inline, .has-error .checkbox-inline, .has-error.radio label, .has-error.checkbox label, .has-error.radio-inline label, .has-error.checkbox-inline label { color: #a94442; } .has-error .form-control { border-color: #a94442; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); } .has-error .form-control:focus { border-color: #843534; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; } .has-error .input-group-addon { color: #a94442; background-color: #f2dede; border-color: #a94442; } .has-error .form-control-feedback { color: #a94442; } .has-feedback label ~ .form-control-feedback { top: 25px; } .has-feedback label.sr-only ~ .form-control-feedback { top: 0; } .help-block { display: block; margin-top: 5px; margin-bottom: 10px; color: #737373; } @media (min-width: 768px) { .form-inline .form-group { display: inline-block; margin-bottom: 0; vertical-align: middle; } .form-inline .form-control { display: inline-block; width: auto; vertical-align: middle; } .form-inline .form-control-static { display: inline-block; } .form-inline .input-group { display: inline-table; vertical-align: middle; } .form-inline .input-group .input-group-addon, .form-inline .input-group .input-group-btn, .form-inline .input-group .form-control { width: auto; } .form-inline .input-group > .form-control { width: 100%; } .form-inline .control-label { margin-bottom: 0; vertical-align: middle; } .form-inline .radio, .form-inline .checkbox { display: inline-block; margin-top: 0; margin-bottom: 0; vertical-align: middle; } .form-inline .radio label, .form-inline .checkbox label { padding-left: 0; } .form-inline .radio input[type="radio"], .form-inline .checkbox input[type="checkbox"] { position: relative; margin-left: 0; } .form-inline .has-feedback .form-control-feedback { top: 0; } } .form-horizontal .radio, .form-horizontal .checkbox, .form-horizontal .radio-inline, .form-horizontal .checkbox-inline { padding-top: 7px; margin-top: 0; margin-bottom: 0; } .form-horizontal .radio, .form-horizontal .checkbox { min-height: 27px; } .form-horizontal .form-group { margin-right: -15px; margin-left: -15px; } @media (min-width: 768px) { .form-horizontal .control-label { padding-top: 7px; margin-bottom: 0; text-align: right; } } .form-horizontal .has-feedback .form-control-feedback { right: 15px; } @media (min-width: 768px) { .form-horizontal .form-group-lg .control-label { padding-top: 14.333333px; font-size: 18px; } } @media (min-width: 768px) { .form-horizontal .form-group-sm .control-label { padding-top: 6px; font-size: 12px; } } .btn { display: inline-block; padding: 6px 12px; margin-bottom: 0; font-size: 14px; font-weight: normal; line-height: 1.42857143; text-align: center; white-space: nowrap; vertical-align: middle; -ms-touch-action: manipulation; touch-action: manipulation; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; background-image: none; border: 1px solid transparent; border-radius: 4px; } .btn:focus, .btn:active:focus, .btn.active:focus, .btn.focus, .btn:active.focus, .btn.active.focus { outline: thin dotted; outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } .btn:hover, .btn:focus, .btn.focus { color: #333; text-decoration: none; } .btn:active, .btn.active { background-image: none; outline: 0; -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); } .btn.disabled, .btn[disabled], fieldset[disabled] .btn { cursor: not-allowed; filter: alpha(opacity=65); -webkit-box-shadow: none; box-shadow: none; opacity: .65; } a.btn.disabled, fieldset[disabled] a.btn { pointer-events: none; } .btn-default { color: #333; background-color: #fff; border-color: #ccc; } .btn-default:focus, .btn-default.focus { color: #333; background-color: #e6e6e6; border-color: #8c8c8c; } .btn-default:hover { color: #333; background-color: #e6e6e6; border-color: #adadad; } .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { color: #333; background-color: #e6e6e6; border-color: #adadad; } .btn-default:active:hover, .btn-default.active:hover, .open > .dropdown-toggle.btn-default:hover, .btn-default:active:focus, .btn-default.active:focus, .open > .dropdown-toggle.btn-default:focus, .btn-default:active.focus, .btn-default.active.focus, .open > .dropdown-toggle.btn-default.focus { color: #333; background-color: #d4d4d4; border-color: #8c8c8c; } .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { background-image: none; } .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: #fff; border-color: #ccc; } .btn-default .badge { color: #fff; background-color: #333; } .btn-primary { color: #fff; background-color: #337ab7; border-color: #2e6da4; } .btn-primary:focus, .btn-primary.focus { color: #fff; background-color: #286090; border-color: #122b40; } .btn-primary:hover { color: #fff; background-color: #286090; border-color: #204d74; } .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { color: #fff; background-color: #286090; border-color: #204d74; } .btn-primary:active:hover, .btn-primary.active:hover, .open > .dropdown-toggle.btn-primary:hover, .btn-primary:active:focus, .btn-primary.active:focus, .open > .dropdown-toggle.btn-primary:focus, .btn-primary:active.focus, .btn-primary.active.focus, .open > .dropdown-toggle.btn-primary.focus { color: #fff; background-color: #204d74; border-color: #122b40; } .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { background-image: none; } .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: #337ab7; border-color: #2e6da4; } .btn-primary .badge { color: #337ab7; background-color: #fff; } .btn-success { color: #fff; background-color: #5cb85c; border-color: #4cae4c; } .btn-success:focus, .btn-success.focus { color: #fff; background-color: #449d44; border-color: #255625; } .btn-success:hover { color: #fff; background-color: #449d44; border-color: #398439; } .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { color: #fff; background-color: #449d44; border-color: #398439; } .btn-success:active:hover, .btn-success.active:hover, .open > .dropdown-toggle.btn-success:hover, .btn-success:active:focus, .btn-success.active:focus, .open > .dropdown-toggle.btn-success:focus, .btn-success:active.focus, .btn-success.active.focus, .open > .dropdown-toggle.btn-success.focus { color: #fff; background-color: #398439; border-color: #255625; } .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { background-image: none; } .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: #5cb85c; border-color: #4cae4c; } .btn-success .badge { color: #5cb85c; background-color: #fff; } .btn-info { color: #fff; background-color: #5bc0de; border-color: #46b8da; } .btn-info:focus, .btn-info.focus { color: #fff; background-color: #31b0d5; border-color: #1b6d85; } .btn-info:hover { color: #fff; background-color: #31b0d5; border-color: #269abc; } .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { color: #fff; background-color: #31b0d5; border-color: #269abc; } .btn-info:active:hover, .btn-info.active:hover, .open > .dropdown-toggle.btn-info:hover, .btn-info:active:focus, .btn-info.active:focus, .open > .dropdown-toggle.btn-info:focus, .btn-info:active.focus, .btn-info.active.focus, .open > .dropdown-toggle.btn-info.focus { color: #fff; background-color: #269abc; border-color: #1b6d85; } .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { background-image: none; } .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: #5bc0de; border-color: #46b8da; } .btn-info .badge { color: #5bc0de; background-color: #fff; } .btn-warning { color: #fff; background-color: #f0ad4e; border-color: #eea236; } .btn-warning:focus, .btn-warning.focus { color: #fff; background-color: #ec971f; border-color: #985f0d; } .btn-warning:hover { color: #fff; background-color: #ec971f; border-color: #d58512; } .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { color: #fff; background-color: #ec971f; border-color: #d58512; } .btn-warning:active:hover, .btn-warning.active:hover, .open > .dropdown-toggle.btn-warning:hover, .btn-warning:active:focus, .btn-warning.active:focus, .open > .dropdown-toggle.btn-warning:focus, .btn-warning:active.focus, .btn-warning.active.focus, .open > .dropdown-toggle.btn-warning.focus { color: #fff; background-color: #d58512; border-color: #985f0d; } .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { background-image: none; } .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: #f0ad4e; border-color: #eea236; } .btn-warning .badge { color: #f0ad4e; background-color: #fff; } .btn-danger { color: #fff; background-color: #d9534f; border-color: #d43f3a; } .btn-danger:focus, .btn-danger.focus { color: #fff; background-color: #c9302c; border-color: #761c19; } .btn-danger:hover { color: #fff; background-color: #c9302c; border-color: #ac2925; } .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { color: #fff; background-color: #c9302c; border-color: #ac2925; } .btn-danger:active:hover, .btn-danger.active:hover, .open > .dropdown-toggle.btn-danger:hover, .btn-danger:active:focus, .btn-danger.active:focus, .open > .dropdown-toggle.btn-danger:focus, .btn-danger:active.focus, .btn-danger.active.focus, .open > .dropdown-toggle.btn-danger.focus { color: #fff; background-color: #ac2925; border-color: #761c19; } .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { background-image: none; } .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: #d9534f; border-color: #d43f3a; } .btn-danger .badge { color: #d9534f; background-color: #fff; } .btn-link { font-weight: normal; color: #337ab7; border-radius: 0; } .btn-link, .btn-link:active, .btn-link.active, .btn-link[disabled], fieldset[disabled] .btn-link { background-color: transparent; -webkit-box-shadow: none; box-shadow: none; } .btn-link, .btn-link:hover, .btn-link:focus, .btn-link:active { border-color: transparent; } .btn-link:hover, .btn-link:focus { color: #23527c; text-decoration: underline; background-color: transparent; } .btn-link[disabled]:hover, fieldset[disabled] .btn-link:hover, .btn-link[disabled]:focus, fieldset[disabled] .btn-link:focus { color: #777; text-decoration: none; } .btn-lg, .btn-group-lg > .btn { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } .btn-sm, .btn-group-sm > .btn { padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } .btn-xs, .btn-group-xs > .btn { padding: 1px 5px; font-size: 12px; line-height: 1.5; border-radius: 3px; } .btn-block { display: block; width: 100%; } .btn-block + .btn-block { margin-top: 5px; } input[type="submit"].btn-block, input[type="reset"].btn-block, input[type="button"].btn-block { width: 100%; } .fade { opacity: 0; -webkit-transition: opacity .15s linear; -o-transition: opacity .15s linear; transition: opacity .15s linear; } .fade.in { opacity: 1; } .collapse { display: none; } .collapse.in { display: block; } tr.collapse.in { display: table-row; } tbody.collapse.in { display: table-row-group; } .collapsing { position: relative; height: 0; overflow: hidden; -webkit-transition-timing-function: ease; -o-transition-timing-function: ease; transition-timing-function: ease; -webkit-transition-duration: .35s; -o-transition-duration: .35s; transition-duration: .35s; -webkit-transition-property: height, visibility; -o-transition-property: height, visibility; transition-property: height, visibility; } .caret { display: inline-block; width: 0; height: 0; margin-left: 2px; vertical-align: middle; border-top: 4px dashed; border-top: 4px solid \9; border-right: 4px solid transparent; border-left: 4px solid transparent; } .dropup, .dropdown { position: relative; } .dropdown-toggle:focus { outline: 0; } .dropdown-menu { position: absolute; top: 100%; left: 0; z-index: 1000; display: none; float: left; min-width: 160px; padding: 5px 0; margin: 2px 0 0; font-size: 14px; text-align: left; list-style: none; background-color: #fff; -webkit-background-clip: padding-box; background-clip: padding-box; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, .15); border-radius: 4px; -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); box-shadow: 0 6px 12px rgba(0, 0, 0, .175); } .dropdown-menu.pull-right { right: 0; left: auto; } .dropdown-menu .divider { height: 1px; margin: 9px 0; overflow: hidden; background-color: #e5e5e5; } .dropdown-menu > li > a { display: block; padding: 3px 20px; clear: both; font-weight: normal; line-height: 1.42857143; color: #333; white-space: nowrap; } .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { color: #262626; text-decoration: none; background-color: #f5f5f5; } .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { color: #fff; text-decoration: none; background-color: #337ab7; outline: 0; } .dropdown-menu > .disabled > a, .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { color: #777; } .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { text-decoration: none; cursor: not-allowed; background-color: transparent; background-image: none; filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } .open > .dropdown-menu { display: block; } .open > a { outline: 0; } .dropdown-menu-right { right: 0; left: auto; } .dropdown-menu-left { right: auto; left: 0; } .dropdown-header { display: block; padding: 3px 20px; font-size: 12px; line-height: 1.42857143; color: #777; white-space: nowrap; } .dropdown-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 990; } .pull-right > .dropdown-menu { right: 0; left: auto; } .dropup .caret, .navbar-fixed-bottom .dropdown .caret { content: ""; border-top: 0; border-bottom: 4px dashed; border-bottom: 4px solid \9; } .dropup .dropdown-menu, .navbar-fixed-bottom .dropdown .dropdown-menu { top: auto; bottom: 100%; margin-bottom: 2px; } @media (min-width: 768px) { .navbar-right .dropdown-menu { right: 0; left: auto; } .navbar-right .dropdown-menu-left { right: auto; left: 0; } } .btn-group, .btn-group-vertical { position: relative; display: inline-block; vertical-align: middle; } .btn-group > .btn, .btn-group-vertical > .btn { position: relative; float: left; } .btn-group > .btn:hover, .btn-group-vertical > .btn:hover, .btn-group > .btn:focus, .btn-group-vertical > .btn:focus, .btn-group > .btn:active, .btn-group-vertical > .btn:active, .btn-group > .btn.active, .btn-group-vertical > .btn.active { z-index: 2; } .btn-group .btn + .btn, .btn-group .btn + .btn-group, .btn-group .btn-group + .btn, .btn-group .btn-group + .btn-group { margin-left: -1px; } .btn-toolbar { margin-left: -5px; } .btn-toolbar .btn, .btn-toolbar .btn-group, .btn-toolbar .input-group { float: left; } .btn-toolbar > .btn, .btn-toolbar > .btn-group, .btn-toolbar > .input-group { margin-left: 5px; } .btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { border-radius: 0; } .btn-group > .btn:first-child { margin-left: 0; } .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { border-top-right-radius: 0; border-bottom-right-radius: 0; } .btn-group > .btn:last-child:not(:first-child), .btn-group > .dropdown-toggle:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; } .btn-group > .btn-group { float: left; } .btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } .btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, .btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { border-top-right-radius: 0; border-bottom-right-radius: 0; } .btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { border-top-left-radius: 0; border-bottom-left-radius: 0; } .btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { outline: 0; } .btn-group > .btn + .dropdown-toggle { padding-right: 8px; padding-left: 8px; } .btn-group > .btn-lg + .dropdown-toggle { padding-right: 12px; padding-left: 12px; } .btn-group.open .dropdown-toggle { -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); } .btn-group.open .dropdown-toggle.btn-link { -webkit-box-shadow: none; box-shadow: none; } .btn .caret { margin-left: 0; } .btn-lg .caret { border-width: 5px 5px 0; border-bottom-width: 0; } .dropup .btn-lg .caret { border-width: 0 5px 5px; } .btn-group-vertical > .btn, .btn-group-vertical > .btn-group, .btn-group-vertical > .btn-group > .btn { display: block; float: none; width: 100%; max-width: 100%; } .btn-group-vertical > .btn-group > .btn { float: none; } .btn-group-vertical > .btn + .btn, .btn-group-vertical > .btn + .btn-group, .btn-group-vertical > .btn-group + .btn, .btn-group-vertical > .btn-group + .btn-group { margin-top: -1px; margin-left: 0; } .btn-group-vertical > .btn:not(:first-child):not(:last-child) { border-radius: 0; } .btn-group-vertical > .btn:first-child:not(:last-child) { border-top-right-radius: 4px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .btn-group-vertical > .btn:last-child:not(:first-child) { border-top-left-radius: 0; border-top-right-radius: 0; border-bottom-left-radius: 4px; } .btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } .btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, .btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { border-top-left-radius: 0; border-top-right-radius: 0; } .btn-group-justified { display: table; width: 100%; table-layout: fixed; border-collapse: separate; } .btn-group-justified > .btn, .btn-group-justified > .btn-group { display: table-cell; float: none; width: 1%; } .btn-group-justified > .btn-group .btn { width: 100%; } .btn-group-justified > .btn-group .dropdown-menu { left: auto; } [data-toggle="buttons"] > .btn input[type="radio"], [data-toggle="buttons"] > .btn-group > .btn input[type="radio"], [data-toggle="buttons"] > .btn input[type="checkbox"], [data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { position: absolute; clip: rect(0, 0, 0, 0); pointer-events: none; } .input-group { position: relative; display: table; border-collapse: separate; } .input-group[class*="col-"] { float: none; padding-right: 0; padding-left: 0; } .input-group .form-control { position: relative; z-index: 2; float: left; width: 100%; margin-bottom: 0; } .input-group-lg > .form-control, .input-group-lg > .input-group-addon, .input-group-lg > .input-group-btn > .btn { height: 46px; padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } select.input-group-lg > .form-control, select.input-group-lg > .input-group-addon, select.input-group-lg > .input-group-btn > .btn { height: 46px; line-height: 46px; } textarea.input-group-lg > .form-control, textarea.input-group-lg > .input-group-addon, textarea.input-group-lg > .input-group-btn > .btn, select[multiple].input-group-lg > .form-control, select[multiple].input-group-lg > .input-group-addon, select[multiple].input-group-lg > .input-group-btn > .btn { height: auto; } .input-group-sm > .form-control, .input-group-sm > .input-group-addon, .input-group-sm > .input-group-btn > .btn { height: 30px; padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } select.input-group-sm > .form-control, select.input-group-sm > .input-group-addon, select.input-group-sm > .input-group-btn > .btn { height: 30px; line-height: 30px; } textarea.input-group-sm > .form-control, textarea.input-group-sm > .input-group-addon, textarea.input-group-sm > .input-group-btn > .btn, select[multiple].input-group-sm > .form-control, select[multiple].input-group-sm > .input-group-addon, select[multiple].input-group-sm > .input-group-btn > .btn { height: auto; } .input-group-addon, .input-group-btn, .input-group .form-control { display: table-cell; } .input-group-addon:not(:first-child):not(:last-child), .input-group-btn:not(:first-child):not(:last-child), .input-group .form-control:not(:first-child):not(:last-child) { border-radius: 0; } .input-group-addon, .input-group-btn { width: 1%; white-space: nowrap; vertical-align: middle; } .input-group-addon { padding: 6px 12px; font-size: 14px; font-weight: normal; line-height: 1; color: #555; text-align: center; background-color: #eee; border: 1px solid #ccc; border-radius: 4px; } .input-group-addon.input-sm { padding: 5px 10px; font-size: 12px; border-radius: 3px; } .input-group-addon.input-lg { padding: 10px 16px; font-size: 18px; border-radius: 6px; } .input-group-addon input[type="radio"], .input-group-addon input[type="checkbox"] { margin-top: 0; } .input-group .form-control:first-child, .input-group-addon:first-child, .input-group-btn:first-child > .btn, .input-group-btn:first-child > .btn-group > .btn, .input-group-btn:first-child > .dropdown-toggle, .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), .input-group-btn:last-child > .btn-group:not(:last-child) > .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; } .input-group-addon:first-child { border-right: 0; } .input-group .form-control:last-child, .input-group-addon:last-child, .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group > .btn, .input-group-btn:last-child > .dropdown-toggle, .input-group-btn:first-child > .btn:not(:first-child), .input-group-btn:first-child > .btn-group:not(:first-child) > .btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } .input-group-addon:last-child { border-left: 0; } .input-group-btn { position: relative; font-size: 0; white-space: nowrap; } .input-group-btn > .btn { position: relative; } .input-group-btn > .btn + .btn { margin-left: -1px; } .input-group-btn > .btn:hover, .input-group-btn > .btn:focus, .input-group-btn > .btn:active { z-index: 2; } .input-group-btn:first-child > .btn, .input-group-btn:first-child > .btn-group { margin-right: -1px; } .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group { z-index: 2; margin-left: -1px; } .nav { padding-left: 0; margin-bottom: 0; list-style: none; } .nav > li { position: relative; display: block; } .nav > li > a { position: relative; display: block; padding: 10px 15px; } .nav > li > a:hover, .nav > li > a:focus { text-decoration: none; background-color: #eee; } .nav > li.disabled > a { color: #777; } .nav > li.disabled > a:hover, .nav > li.disabled > a:focus { color: #777; text-decoration: none; cursor: not-allowed; background-color: transparent; } .nav .open > a, .nav .open > a:hover, .nav .open > a:focus { background-color: #eee; border-color: #337ab7; } .nav .nav-divider { height: 1px; margin: 9px 0; overflow: hidden; background-color: #e5e5e5; } .nav > li > a > img { max-width: none; } .nav-tabs { border-bottom: 1px solid #ddd; } .nav-tabs > li { float: left; margin-bottom: -1px; } .nav-tabs > li > a { margin-right: 2px; line-height: 1.42857143; border: 1px solid transparent; border-radius: 4px 4px 0 0; } .nav-tabs > li > a:hover { border-color: #eee #eee #ddd; } .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { color: #555; cursor: default; background-color: #fff; border: 1px solid #ddd; border-bottom-color: transparent; } .nav-tabs.nav-justified { width: 100%; border-bottom: 0; } .nav-tabs.nav-justified > li { float: none; } .nav-tabs.nav-justified > li > a { margin-bottom: 5px; text-align: center; } .nav-tabs.nav-justified > .dropdown .dropdown-menu { top: auto; left: auto; } @media (min-width: 768px) { .nav-tabs.nav-justified > li { display: table-cell; width: 1%; } .nav-tabs.nav-justified > li > a { margin-bottom: 0; } } .nav-tabs.nav-justified > li > a { margin-right: 0; border-radius: 4px; } .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, .nav-tabs.nav-justified > .active > a:focus { border: 1px solid #ddd; } @media (min-width: 768px) { .nav-tabs.nav-justified > li > a { border-bottom: 1px solid #ddd; border-radius: 4px 4px 0 0; } .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, .nav-tabs.nav-justified > .active > a:focus { border-bottom-color: #fff; } } .nav-pills > li { float: left; } .nav-pills > li > a { border-radius: 4px; } .nav-pills > li + li { margin-left: 2px; } .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { color: #fff; background-color: #337ab7; } .nav-stacked > li { float: none; } .nav-stacked > li + li { margin-top: 2px; margin-left: 0; } .nav-justified { width: 100%; } .nav-justified > li { float: none; } .nav-justified > li > a { margin-bottom: 5px; text-align: center; } .nav-justified > .dropdown .dropdown-menu { top: auto; left: auto; } @media (min-width: 768px) { .nav-justified > li { display: table-cell; width: 1%; } .nav-justified > li > a { margin-bottom: 0; } } .nav-tabs-justified { border-bottom: 0; } .nav-tabs-justified > li > a { margin-right: 0; border-radius: 4px; } .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > a:focus { border: 1px solid #ddd; } @media (min-width: 768px) { .nav-tabs-justified > li > a { border-bottom: 1px solid #ddd; border-radius: 4px 4px 0 0; } .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > a:focus { border-bottom-color: #fff; } } .tab-content > .tab-pane { display: none; } .tab-content > .active { display: block; } .nav-tabs .dropdown-menu { margin-top: -1px; border-top-left-radius: 0; border-top-right-radius: 0; } .navbar { position: relative; min-height: 50px; margin-bottom: 20px; border: 1px solid transparent; } @media (min-width: 768px) { .navbar { border-radius: 4px; } } @media (min-width: 768px) { .navbar-header { float: left; } } .navbar-collapse { padding-right: 15px; padding-left: 15px; overflow-x: visible; -webkit-overflow-scrolling: touch; border-top: 1px solid transparent; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); } .navbar-collapse.in { overflow-y: auto; } @media (min-width: 768px) { .navbar-collapse { width: auto; border-top: 0; -webkit-box-shadow: none; box-shadow: none; } .navbar-collapse.collapse { display: block !important; height: auto !important; padding-bottom: 0; overflow: visible !important; } .navbar-collapse.in { overflow-y: visible; } .navbar-fixed-top .navbar-collapse, .navbar-static-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse { padding-right: 0; padding-left: 0; } } .navbar-fixed-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse { max-height: 340px; } @media (max-device-width: 480px) and (orientation: landscape) { .navbar-fixed-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse { max-height: 200px; } } .container > .navbar-header, .container-fluid > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-collapse { margin-right: -15px; margin-left: -15px; } @media (min-width: 768px) { .container > .navbar-header, .container-fluid > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-collapse { margin-right: 0; margin-left: 0; } } .navbar-static-top { z-index: 1000; border-width: 0 0 1px; } @media (min-width: 768px) { .navbar-static-top { border-radius: 0; } } .navbar-fixed-top, .navbar-fixed-bottom { position: fixed; right: 0; left: 0; z-index: 1030; } @media (min-width: 768px) { .navbar-fixed-top, .navbar-fixed-bottom { border-radius: 0; } } .navbar-fixed-top { top: 0; border-width: 0 0 1px; } .navbar-fixed-bottom { bottom: 0; margin-bottom: 0; border-width: 1px 0 0; } .navbar-brand { float: left; height: 50px; padding: 15px 15px; font-size: 18px; line-height: 20px; } .navbar-brand:hover, .navbar-brand:focus { text-decoration: none; } .navbar-brand > img { display: block; } @media (min-width: 768px) { .navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand { margin-left: -15px; } } .navbar-toggle { position: relative; float: right; padding: 9px 10px; margin-top: 8px; margin-right: 15px; margin-bottom: 8px; background-color: transparent; background-image: none; border: 1px solid transparent; border-radius: 4px; } .navbar-toggle:focus { outline: 0; } .navbar-toggle .icon-bar { display: block; width: 22px; height: 2px; border-radius: 1px; } .navbar-toggle .icon-bar + .icon-bar { margin-top: 4px; } @media (min-width: 768px) { .navbar-toggle { display: none; } } .navbar-nav { margin: 7.5px -15px; } .navbar-nav > li > a { padding-top: 10px; padding-bottom: 10px; line-height: 20px; } @media (max-width: 767px) { .navbar-nav .open .dropdown-menu { position: static; float: none; width: auto; margin-top: 0; background-color: transparent; border: 0; -webkit-box-shadow: none; box-shadow: none; } .navbar-nav .open .dropdown-menu > li > a, .navbar-nav .open .dropdown-menu .dropdown-header { padding: 5px 15px 5px 25px; } .navbar-nav .open .dropdown-menu > li > a { line-height: 20px; } .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-nav .open .dropdown-menu > li > a:focus { background-image: none; } } @media (min-width: 768px) { .navbar-nav { float: left; margin: 0; } .navbar-nav > li { float: left; } .navbar-nav > li > a { padding-top: 15px; padding-bottom: 15px; } } .navbar-form { padding: 10px 15px; margin-top: 8px; margin-right: -15px; margin-bottom: 8px; margin-left: -15px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); } @media (min-width: 768px) { .navbar-form .form-group { display: inline-block; margin-bottom: 0; vertical-align: middle; } .navbar-form .form-control { display: inline-block; width: auto; vertical-align: middle; } .navbar-form .form-control-static { display: inline-block; } .navbar-form .input-group { display: inline-table; vertical-align: middle; } .navbar-form .input-group .input-group-addon, .navbar-form .input-group .input-group-btn, .navbar-form .input-group .form-control { width: auto; } .navbar-form .input-group > .form-control { width: 100%; } .navbar-form .control-label { margin-bottom: 0; vertical-align: middle; } .navbar-form .radio, .navbar-form .checkbox { display: inline-block; margin-top: 0; margin-bottom: 0; vertical-align: middle; } .navbar-form .radio label, .navbar-form .checkbox label { padding-left: 0; } .navbar-form .radio input[type="radio"], .navbar-form .checkbox input[type="checkbox"] { position: relative; margin-left: 0; } .navbar-form .has-feedback .form-control-feedback { top: 0; } } @media (max-width: 767px) { .navbar-form .form-group { margin-bottom: 5px; } .navbar-form .form-group:last-child { margin-bottom: 0; } } @media (min-width: 768px) { .navbar-form { width: auto; padding-top: 0; padding-bottom: 0; margin-right: 0; margin-left: 0; border: 0; -webkit-box-shadow: none; box-shadow: none; } } .navbar-nav > li > .dropdown-menu { margin-top: 0; border-top-left-radius: 0; border-top-right-radius: 0; } .navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { margin-bottom: 0; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .navbar-btn { margin-top: 8px; margin-bottom: 8px; } .navbar-btn.btn-sm { margin-top: 10px; margin-bottom: 10px; } .navbar-btn.btn-xs { margin-top: 14px; margin-bottom: 14px; } .navbar-text { margin-top: 15px; margin-bottom: 15px; } @media (min-width: 768px) { .navbar-text { float: left; margin-right: 15px; margin-left: 15px; } } @media (min-width: 768px) { .navbar-left { float: left !important; } .navbar-right { float: right !important; margin-right: -15px; } .navbar-right ~ .navbar-right { margin-right: 0; } } .navbar-default { background-color: #f8f8f8; border-color: #e7e7e7; } .navbar-default .navbar-brand { color: #777; } .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus { color: #5e5e5e; background-color: transparent; } .navbar-default .navbar-text { color: #777; } .navbar-default .navbar-nav > li > a { color: #777; } .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { color: #333; background-color: transparent; } .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { color: #555; background-color: #e7e7e7; } .navbar-default .navbar-nav > .disabled > a, .navbar-default .navbar-nav > .disabled > a:hover, .navbar-default .navbar-nav > .disabled > a:focus { color: #ccc; background-color: transparent; } .navbar-default .navbar-toggle { border-color: #ddd; } .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { background-color: #ddd; } .navbar-default .navbar-toggle .icon-bar { background-color: #888; } .navbar-default .navbar-collapse, .navbar-default .navbar-form { border-color: #e7e7e7; } .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { color: #555; background-color: #e7e7e7; } @media (max-width: 767px) { .navbar-default .navbar-nav .open .dropdown-menu > li > a { color: #777; } .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { color: #333; background-color: transparent; } .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { color: #555; background-color: #e7e7e7; } .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { color: #ccc; background-color: transparent; } } .navbar-default .navbar-link { color: #777; } .navbar-default .navbar-link:hover { color: #333; } .navbar-default .btn-link { color: #777; } .navbar-default .btn-link:hover, .navbar-default .btn-link:focus { color: #333; } .navbar-default .btn-link[disabled]:hover, fieldset[disabled] .navbar-default .btn-link:hover, .navbar-default .btn-link[disabled]:focus, fieldset[disabled] .navbar-default .btn-link:focus { color: #ccc; } .navbar-inverse { background-color: #222; border-color: #080808; } .navbar-inverse .navbar-brand { color: #9d9d9d; } .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus { color: #fff; background-color: transparent; } .navbar-inverse .navbar-text { color: #9d9d9d; } .navbar-inverse .navbar-nav > li > a { color: #9d9d9d; } .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus { color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus { color: #fff; background-color: #080808; } .navbar-inverse .navbar-nav > .disabled > a, .navbar-inverse .navbar-nav > .disabled > a:hover, .navbar-inverse .navbar-nav > .disabled > a:focus { color: #444; background-color: transparent; } .navbar-inverse .navbar-toggle { border-color: #333; } .navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus { background-color: #333; } .navbar-inverse .navbar-toggle .icon-bar { background-color: #fff; } .navbar-inverse .navbar-collapse, .navbar-inverse .navbar-form { border-color: #101010; } .navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus { color: #fff; background-color: #080808; } @media (max-width: 767px) { .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { border-color: #080808; } .navbar-inverse .navbar-nav .open .dropdown-menu .divider { background-color: #080808; } .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { color: #9d9d9d; } .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { color: #fff; background-color: #080808; } .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { color: #444; background-color: transparent; } } .navbar-inverse .navbar-link { color: #9d9d9d; } .navbar-inverse .navbar-link:hover { color: #fff; } .navbar-inverse .btn-link { color: #9d9d9d; } .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link:focus { color: #fff; } .navbar-inverse .btn-link[disabled]:hover, fieldset[disabled] .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link[disabled]:focus, fieldset[disabled] .navbar-inverse .btn-link:focus { color: #444; } .breadcrumb { padding: 8px 15px; margin-bottom: 20px; list-style: none; background-color: #f5f5f5; border-radius: 4px; } .breadcrumb > li { display: inline-block; } .breadcrumb > li + li:before { padding: 0 5px; color: #ccc; content: "/\00a0"; } .breadcrumb > .active { color: #777; } .pagination { display: inline-block; padding-left: 0; margin: 20px 0; border-radius: 4px; } .pagination > li { display: inline; } .pagination > li > a, .pagination > li > span { position: relative; float: left; padding: 6px 12px; margin-left: -1px; line-height: 1.42857143; color: #337ab7; text-decoration: none; background-color: #fff; border: 1px solid #ddd; } .pagination > li:first-child > a, .pagination > li:first-child > span { margin-left: 0; border-top-left-radius: 4px; border-bottom-left-radius: 4px; } .pagination > li:last-child > a, .pagination > li:last-child > span { border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .pagination > li > a:hover, .pagination > li > span:hover, .pagination > li > a:focus, .pagination > li > span:focus { z-index: 3; color: #23527c; background-color: #eee; border-color: #ddd; } .pagination > .active > a, .pagination > .active > span, .pagination > .active > a:hover, .pagination > .active > span:hover, .pagination > .active > a:focus, .pagination > .active > span:focus { z-index: 2; color: #fff; cursor: default; background-color: #337ab7; border-color: #337ab7; } .pagination > .disabled > span, .pagination > .disabled > span:hover, .pagination > .disabled > span:focus, .pagination > .disabled > a, .pagination > .disabled > a:hover, .pagination > .disabled > a:focus { color: #777; cursor: not-allowed; background-color: #fff; border-color: #ddd; } .pagination-lg > li > a, .pagination-lg > li > span { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; } .pagination-lg > li:first-child > a, .pagination-lg > li:first-child > span { border-top-left-radius: 6px; border-bottom-left-radius: 6px; } .pagination-lg > li:last-child > a, .pagination-lg > li:last-child > span { border-top-right-radius: 6px; border-bottom-right-radius: 6px; } .pagination-sm > li > a, .pagination-sm > li > span { padding: 5px 10px; font-size: 12px; line-height: 1.5; } .pagination-sm > li:first-child > a, .pagination-sm > li:first-child > span { border-top-left-radius: 3px; border-bottom-left-radius: 3px; } .pagination-sm > li:last-child > a, .pagination-sm > li:last-child > span { border-top-right-radius: 3px; border-bottom-right-radius: 3px; } .pager { padding-left: 0; margin: 20px 0; text-align: center; list-style: none; } .pager li { display: inline; } .pager li > a, .pager li > span { display: inline-block; padding: 5px 14px; background-color: #fff; border: 1px solid #ddd; border-radius: 15px; } .pager li > a:hover, .pager li > a:focus { text-decoration: none; background-color: #eee; } .pager .next > a, .pager .next > span { float: right; } .pager .previous > a, .pager .previous > span { float: left; } .pager .disabled > a, .pager .disabled > a:hover, .pager .disabled > a:focus, .pager .disabled > span { color: #777; cursor: not-allowed; background-color: #fff; } .label { display: inline; padding: .2em .6em .3em; font-size: 75%; font-weight: bold; line-height: 1; color: #fff; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: .25em; } a.label:hover, a.label:focus { color: #fff; text-decoration: none; cursor: pointer; } .label:empty { display: none; } .btn .label { position: relative; top: -1px; } .label-default { background-color: #777; } .label-default[href]:hover, .label-default[href]:focus { background-color: #5e5e5e; } .label-primary { background-color: #337ab7; } .label-primary[href]:hover, .label-primary[href]:focus { background-color: #286090; } .label-success { background-color: #5cb85c; } .label-success[href]:hover, .label-success[href]:focus { background-color: #449d44; } .label-info { background-color: #5bc0de; } .label-info[href]:hover, .label-info[href]:focus { background-color: #31b0d5; } .label-warning { background-color: #f0ad4e; } .label-warning[href]:hover, .label-warning[href]:focus { background-color: #ec971f; } .label-danger { background-color: #d9534f; } .label-danger[href]:hover, .label-danger[href]:focus { background-color: #c9302c; } .badge { display: inline-block; min-width: 10px; padding: 3px 7px; font-size: 12px; font-weight: bold; line-height: 1; color: #fff; text-align: center; white-space: nowrap; vertical-align: middle; background-color: #777; border-radius: 10px; } .badge:empty { display: none; } .btn .badge { position: relative; top: -1px; } .btn-xs .badge, .btn-group-xs > .btn .badge { top: 0; padding: 1px 5px; } a.badge:hover, a.badge:focus { color: #fff; text-decoration: none; cursor: pointer; } .list-group-item.active > .badge, .nav-pills > .active > a > .badge { color: #337ab7; background-color: #fff; } .list-group-item > .badge { float: right; } .list-group-item > .badge + .badge { margin-right: 5px; } .nav-pills > li > a > .badge { margin-left: 3px; } .jumbotron { padding-top: 30px; padding-bottom: 30px; margin-bottom: 30px; color: inherit; background-color: #eee; } .jumbotron h1, .jumbotron .h1 { color: inherit; } .jumbotron p { margin-bottom: 15px; font-size: 21px; font-weight: 200; } .jumbotron > hr { border-top-color: #d5d5d5; } .container .jumbotron, .container-fluid .jumbotron { border-radius: 6px; } .jumbotron .container { max-width: 100%; } @media screen and (min-width: 768px) { .jumbotron { padding-top: 48px; padding-bottom: 48px; } .container .jumbotron, .container-fluid .jumbotron { padding-right: 60px; padding-left: 60px; } .jumbotron h1, .jumbotron .h1 { font-size: 63px; } } .thumbnail { display: block; padding: 4px; margin-bottom: 20px; line-height: 1.42857143; background-color: #fff; border: 1px solid #ddd; border-radius: 4px; -webkit-transition: border .2s ease-in-out; -o-transition: border .2s ease-in-out; transition: border .2s ease-in-out; } .thumbnail > img, .thumbnail a > img { margin-right: auto; margin-left: auto; } a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active { border-color: #337ab7; } .thumbnail .caption { padding: 9px; color: #333; } .alert { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; } .alert h4 { margin-top: 0; color: inherit; } .alert .alert-link { font-weight: bold; } .alert > p, .alert > ul { margin-bottom: 0; } .alert > p + p { margin-top: 5px; } .alert-dismissable, .alert-dismissible { padding-right: 35px; } .alert-dismissable .close, .alert-dismissible .close { position: relative; top: -2px; right: -21px; color: inherit; } .alert-success { color: #3c763d; background-color: #dff0d8; border-color: #d6e9c6; } .alert-success hr { border-top-color: #c9e2b3; } .alert-success .alert-link { color: #2b542c; } .alert-info { color: #31708f; background-color: #d9edf7; border-color: #bce8f1; } .alert-info hr { border-top-color: #a6e1ec; } .alert-info .alert-link { color: #245269; } .alert-warning { color: #8a6d3b; background-color: #fcf8e3; border-color: #faebcc; } .alert-warning hr { border-top-color: #f7e1b5; } .alert-warning .alert-link { color: #66512c; } .alert-danger { color: #a94442; background-color: #f2dede; border-color: #ebccd1; } .alert-danger hr { border-top-color: #e4b9c0; } .alert-danger .alert-link { color: #843534; } @-webkit-keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } @-o-keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } @keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } .progress { height: 20px; margin-bottom: 20px; overflow: hidden; background-color: #f5f5f5; border-radius: 4px; -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); } .progress-bar { float: left; width: 0; height: 100%; font-size: 12px; line-height: 20px; color: #fff; text-align: center; background-color: #337ab7; -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); -webkit-transition: width .6s ease; -o-transition: width .6s ease; transition: width .6s ease; } .progress-striped .progress-bar, .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); -webkit-background-size: 40px 40px; background-size: 40px 40px; } .progress.active .progress-bar, .progress-bar.active { -webkit-animation: progress-bar-stripes 2s linear infinite; -o-animation: progress-bar-stripes 2s linear infinite; animation: progress-bar-stripes 2s linear infinite; } .progress-bar-success { background-color: #5cb85c; } .progress-striped .progress-bar-success { 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); } .progress-bar-info { background-color: #5bc0de; } .progress-striped .progress-bar-info { 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); } .progress-bar-warning { background-color: #f0ad4e; } .progress-striped .progress-bar-warning { 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); } .progress-bar-danger { background-color: #d9534f; } .progress-striped .progress-bar-danger { 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); } .media { margin-top: 15px; } .media:first-child { margin-top: 0; } .media, .media-body { overflow: hidden; zoom: 1; } .media-body { width: 10000px; } .media-object { display: block; } .media-object.img-thumbnail { max-width: none; } .media-right, .media > .pull-right { padding-left: 10px; } .media-left, .media > .pull-left { padding-right: 10px; } .media-left, .media-right, .media-body { display: table-cell; vertical-align: top; } .media-middle { vertical-align: middle; } .media-bottom { vertical-align: bottom; } .media-heading { margin-top: 0; margin-bottom: 5px; } .media-list { padding-left: 0; list-style: none; } .list-group { padding-left: 0; margin-bottom: 20px; } .list-group-item { position: relative; display: block; padding: 10px 15px; margin-bottom: -1px; background-color: #fff; border: 1px solid #ddd; } .list-group-item:first-child { border-top-left-radius: 4px; border-top-right-radius: 4px; } .list-group-item:last-child { margin-bottom: 0; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; } a.list-group-item, button.list-group-item { color: #555; } a.list-group-item .list-group-item-heading, button.list-group-item .list-group-item-heading { color: #333; } a.list-group-item:hover, button.list-group-item:hover, a.list-group-item:focus, button.list-group-item:focus { color: #555; text-decoration: none; background-color: #f5f5f5; } button.list-group-item { width: 100%; text-align: left; } .list-group-item.disabled, .list-group-item.disabled:hover, .list-group-item.disabled:focus { color: #777; cursor: not-allowed; background-color: #eee; } .list-group-item.disabled .list-group-item-heading, .list-group-item.disabled:hover .list-group-item-heading, .list-group-item.disabled:focus .list-group-item-heading { color: inherit; } .list-group-item.disabled .list-group-item-text, .list-group-item.disabled:hover .list-group-item-text, .list-group-item.disabled:focus .list-group-item-text { color: #777; } .list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { z-index: 2; color: #fff; background-color: #337ab7; border-color: #337ab7; } .list-group-item.active .list-group-item-heading, .list-group-item.active:hover .list-group-item-heading, .list-group-item.active:focus .list-group-item-heading, .list-group-item.active .list-group-item-heading > small, .list-group-item.active:hover .list-group-item-heading > small, .list-group-item.active:focus .list-group-item-heading > small, .list-group-item.active .list-group-item-heading > .small, .list-group-item.active:hover .list-group-item-heading > .small, .list-group-item.active:focus .list-group-item-heading > .small { color: inherit; } .list-group-item.active .list-group-item-text, .list-group-item.active:hover .list-group-item-text, .list-group-item.active:focus .list-group-item-text { color: #c7ddef; } .list-group-item-success { color: #3c763d; background-color: #dff0d8; } a.list-group-item-success, button.list-group-item-success { color: #3c763d; } a.list-group-item-success .list-group-item-heading, button.list-group-item-success .list-group-item-heading { color: inherit; } a.list-group-item-success:hover, button.list-group-item-success:hover, a.list-group-item-success:focus, button.list-group-item-success:focus { color: #3c763d; background-color: #d0e9c6; } a.list-group-item-success.active, button.list-group-item-success.active, a.list-group-item-success.active:hover, button.list-group-item-success.active:hover, a.list-group-item-success.active:focus, button.list-group-item-success.active:focus { color: #fff; background-color: #3c763d; border-color: #3c763d; } .list-group-item-info { color: #31708f; background-color: #d9edf7; } a.list-group-item-info, button.list-group-item-info { color: #31708f; } a.list-group-item-info .list-group-item-heading, button.list-group-item-info .list-group-item-heading { color: inherit; } a.list-group-item-info:hover, button.list-group-item-info:hover, a.list-group-item-info:focus, button.list-group-item-info:focus { color: #31708f; background-color: #c4e3f3; } a.list-group-item-info.active, button.list-group-item-info.active, a.list-group-item-info.active:hover, button.list-group-item-info.active:hover, a.list-group-item-info.active:focus, button.list-group-item-info.active:focus { color: #fff; background-color: #31708f; border-color: #31708f; } .list-group-item-warning { color: #8a6d3b; background-color: #fcf8e3; } a.list-group-item-warning, button.list-group-item-warning { color: #8a6d3b; } a.list-group-item-warning .list-group-item-heading, button.list-group-item-warning .list-group-item-heading { color: inherit; } a.list-group-item-warning:hover, button.list-group-item-warning:hover, a.list-group-item-warning:focus, button.list-group-item-warning:focus { color: #8a6d3b; background-color: #faf2cc; } a.list-group-item-warning.active, button.list-group-item-warning.active, a.list-group-item-warning.active:hover, button.list-group-item-warning.active:hover, a.list-group-item-warning.active:focus, button.list-group-item-warning.active:focus { color: #fff; background-color: #8a6d3b; border-color: #8a6d3b; } .list-group-item-danger { color: #a94442; background-color: #f2dede; } a.list-group-item-danger, button.list-group-item-danger { color: #a94442; } a.list-group-item-danger .list-group-item-heading, button.list-group-item-danger .list-group-item-heading { color: inherit; } a.list-group-item-danger:hover, button.list-group-item-danger:hover, a.list-group-item-danger:focus, button.list-group-item-danger:focus { color: #a94442; background-color: #ebcccc; } a.list-group-item-danger.active, button.list-group-item-danger.active, a.list-group-item-danger.active:hover, button.list-group-item-danger.active:hover, a.list-group-item-danger.active:focus, button.list-group-item-danger.active:focus { color: #fff; background-color: #a94442; border-color: #a94442; } .list-group-item-heading { margin-top: 0; margin-bottom: 5px; } .list-group-item-text { margin-bottom: 0; line-height: 1.3; } .panel { margin-bottom: 20px; background-color: #fff; border: 1px solid transparent; border-radius: 4px; -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); box-shadow: 0 1px 1px rgba(0, 0, 0, .05); } .panel-body { padding: 15px; } .panel-heading { padding: 10px 15px; border-bottom: 1px solid transparent; border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel-heading > .dropdown .dropdown-toggle { color: inherit; } .panel-title { margin-top: 0; margin-bottom: 0; font-size: 16px; color: inherit; } .panel-title > a, .panel-title > small, .panel-title > .small, .panel-title > small > a, .panel-title > .small > a { color: inherit; } .panel-footer { padding: 10px 15px; background-color: #f5f5f5; border-top: 1px solid #ddd; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .list-group, .panel > .panel-collapse > .list-group { margin-bottom: 0; } .panel > .list-group .list-group-item, .panel > .panel-collapse > .list-group .list-group-item { border-width: 1px 0; border-radius: 0; } .panel > .list-group:first-child .list-group-item:first-child, .panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { border-top: 0; border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel > .list-group:last-child .list-group-item:last-child, .panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { border-bottom: 0; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { border-top-left-radius: 0; border-top-right-radius: 0; } .panel-heading + .list-group .list-group-item:first-child { border-top-width: 0; } .list-group + .panel-footer { border-top-width: 0; } .panel > .table, .panel > .table-responsive > .table, .panel > .panel-collapse > .table { margin-bottom: 0; } .panel > .table caption, .panel > .table-responsive > .table caption, .panel > .panel-collapse > .table caption { padding-right: 15px; padding-left: 15px; } .panel > .table:first-child, .panel > .table-responsive:first-child > .table:first-child { border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel > .table:first-child > thead:first-child > tr:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { border-top-left-radius: 3px; border-top-right-radius: 3px; } .panel > .table:first-child > thead:first-child > tr:first-child td:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, .panel > .table:first-child > thead:first-child > tr:first-child th:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { border-top-left-radius: 3px; } .panel > .table:first-child > thead:first-child > tr:first-child td:last-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, .panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, .panel > .table:first-child > thead:first-child > tr:first-child th:last-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, .panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { border-top-right-radius: 3px; } .panel > .table:last-child, .panel > .table-responsive:last-child > .table:last-child { border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .table:last-child > tbody:last-child > tr:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } .panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, .panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, .panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, .panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { border-bottom-left-radius: 3px; } .panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, .panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { border-bottom-right-radius: 3px; } .panel > .panel-body + .table, .panel > .panel-body + .table-responsive, .panel > .table + .panel-body, .panel > .table-responsive + .panel-body { border-top: 1px solid #ddd; } .panel > .table > tbody:first-child > tr:first-child th, .panel > .table > tbody:first-child > tr:first-child td { border-top: 0; } .panel > .table-bordered, .panel > .table-responsive > .table-bordered { border: 0; } .panel > .table-bordered > thead > tr > th:first-child, .panel > .table-responsive > .table-bordered > thead > tr > th:first-child, .panel > .table-bordered > tbody > tr > th:first-child, .panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, .panel > .table-bordered > tfoot > tr > th:first-child, .panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, .panel > .table-bordered > thead > tr > td:first-child, .panel > .table-responsive > .table-bordered > thead > tr > td:first-child, .panel > .table-bordered > tbody > tr > td:first-child, .panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, .panel > .table-bordered > tfoot > tr > td:first-child, .panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { border-left: 0; } .panel > .table-bordered > thead > tr > th:last-child, .panel > .table-responsive > .table-bordered > thead > tr > th:last-child, .panel > .table-bordered > tbody > tr > th:last-child, .panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, .panel > .table-bordered > tfoot > tr > th:last-child, .panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, .panel > .table-bordered > thead > tr > td:last-child, .panel > .table-responsive > .table-bordered > thead > tr > td:last-child, .panel > .table-bordered > tbody > tr > td:last-child, .panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, .panel > .table-bordered > tfoot > tr > td:last-child, .panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { border-right: 0; } .panel > .table-bordered > thead > tr:first-child > td, .panel > .table-responsive > .table-bordered > thead > tr:first-child > td, .panel > .table-bordered > tbody > tr:first-child > td, .panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, .panel > .table-bordered > thead > tr:first-child > th, .panel > .table-responsive > .table-bordered > thead > tr:first-child > th, .panel > .table-bordered > tbody > tr:first-child > th, .panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { border-bottom: 0; } .panel > .table-bordered > tbody > tr:last-child > td, .panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, .panel > .table-bordered > tfoot > tr:last-child > td, .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, .panel > .table-bordered > tbody > tr:last-child > th, .panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, .panel > .table-bordered > tfoot > tr:last-child > th, .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { border-bottom: 0; } .panel > .table-responsive { margin-bottom: 0; border: 0; } .panel-group { margin-bottom: 20px; } .panel-group .panel { margin-bottom: 0; border-radius: 4px; } .panel-group .panel + .panel { margin-top: 5px; } .panel-group .panel-heading { border-bottom: 0; } .panel-group .panel-heading + .panel-collapse > .panel-body, .panel-group .panel-heading + .panel-collapse > .list-group { border-top: 1px solid #ddd; } .panel-group .panel-footer { border-top: 0; } .panel-group .panel-footer + .panel-collapse .panel-body { border-bottom: 1px solid #ddd; } .panel-default { border-color: #ddd; } .panel-default > .panel-heading { color: #333; background-color: #f5f5f5; border-color: #ddd; } .panel-default > .panel-heading + .panel-collapse > .panel-body { border-top-color: #ddd; } .panel-default > .panel-heading .badge { color: #f5f5f5; background-color: #333; } .panel-default > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #ddd; } .panel-primary { border-color: #337ab7; } .panel-primary > .panel-heading { color: #fff; background-color: #337ab7; border-color: #337ab7; } .panel-primary > .panel-heading + .panel-collapse > .panel-body { border-top-color: #337ab7; } .panel-primary > .panel-heading .badge { color: #337ab7; background-color: #fff; } .panel-primary > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #337ab7; } .panel-success { border-color: #d6e9c6; } .panel-success > .panel-heading { color: #3c763d; background-color: #dff0d8; border-color: #d6e9c6; } .panel-success > .panel-heading + .panel-collapse > .panel-body { border-top-color: #d6e9c6; } .panel-success > .panel-heading .badge { color: #dff0d8; background-color: #3c763d; } .panel-success > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #d6e9c6; } .panel-info { border-color: #bce8f1; } .panel-info > .panel-heading { color: #31708f; background-color: #d9edf7; border-color: #bce8f1; } .panel-info > .panel-heading + .panel-collapse > .panel-body { border-top-color: #bce8f1; } .panel-info > .panel-heading .badge { color: #d9edf7; background-color: #31708f; } .panel-info > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #bce8f1; } .panel-warning { border-color: #faebcc; } .panel-warning > .panel-heading { color: #8a6d3b; background-color: #fcf8e3; border-color: #faebcc; } .panel-warning > .panel-heading + .panel-collapse > .panel-body { border-top-color: #faebcc; } .panel-warning > .panel-heading .badge { color: #fcf8e3; background-color: #8a6d3b; } .panel-warning > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #faebcc; } .panel-danger { border-color: #ebccd1; } .panel-danger > .panel-heading { color: #a94442; background-color: #f2dede; border-color: #ebccd1; } .panel-danger > .panel-heading + .panel-collapse > .panel-body { border-top-color: #ebccd1; } .panel-danger > .panel-heading .badge { color: #f2dede; background-color: #a94442; } .panel-danger > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #ebccd1; } .embed-responsive { position: relative; display: block; height: 0; padding: 0; overflow: hidden; } .embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object, .embed-responsive video { position: absolute; top: 0; bottom: 0; left: 0; width: 100%; height: 100%; border: 0; } .embed-responsive-16by9 { padding-bottom: 56.25%; } .embed-responsive-4by3 { padding-bottom: 75%; } .well { min-height: 20px; padding: 19px; margin-bottom: 20px; background-color: #f5f5f5; border: 1px solid #e3e3e3; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); } .well blockquote { border-color: #ddd; border-color: rgba(0, 0, 0, .15); } .well-lg { padding: 24px; border-radius: 6px; } .well-sm { padding: 9px; border-radius: 3px; } .close { float: right; font-size: 21px; font-weight: bold; line-height: 1; color: #000; text-shadow: 0 1px 0 #fff; filter: alpha(opacity=20); opacity: .2; } .close:hover, .close:focus { color: #000; text-decoration: none; cursor: pointer; filter: alpha(opacity=50); opacity: .5; } button.close { -webkit-appearance: none; padding: 0; cursor: pointer; background: transparent; border: 0; } .modal-open { overflow: hidden; } .modal { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1050; display: none; overflow: hidden; -webkit-overflow-scrolling: touch; outline: 0; } .modal.fade .modal-dialog { -webkit-transition: -webkit-transform .3s ease-out; -o-transition: -o-transform .3s ease-out; transition: transform .3s ease-out; -webkit-transform: translate(0, -25%); -ms-transform: translate(0, -25%); -o-transform: translate(0, -25%); transform: translate(0, -25%); } .modal.in .modal-dialog { -webkit-transform: translate(0, 0); -ms-transform: translate(0, 0); -o-transform: translate(0, 0); transform: translate(0, 0); } .modal-open .modal { overflow-x: hidden; overflow-y: auto; } .modal-dialog { position: relative; width: auto; margin: 10px; } .modal-content { position: relative; background-color: #fff; -webkit-background-clip: padding-box; background-clip: padding-box; border: 1px solid #999; border: 1px solid rgba(0, 0, 0, .2); border-radius: 6px; outline: 0; -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); box-shadow: 0 3px 9px rgba(0, 0, 0, .5); } .modal-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1040; background-color: #000; } .modal-backdrop.fade { filter: alpha(opacity=0); opacity: 0; } .modal-backdrop.in { filter: alpha(opacity=50); opacity: .5; } .modal-header { min-height: 16.42857143px; padding: 15px; border-bottom: 1px solid #e5e5e5; } .modal-header .close { margin-top: -2px; } .modal-title { margin: 0; line-height: 1.42857143; } .modal-body { position: relative; padding: 15px; } .modal-footer { padding: 15px; text-align: right; border-top: 1px solid #e5e5e5; } .modal-footer .btn + .btn { margin-bottom: 0; margin-left: 5px; } .modal-footer .btn-group .btn + .btn { margin-left: -1px; } .modal-footer .btn-block + .btn-block { margin-left: 0; } .modal-scrollbar-measure { position: absolute; top: -9999px; width: 50px; height: 50px; overflow: scroll; } @media (min-width: 768px) { .modal-dialog { width: 600px; margin: 30px auto; } .modal-content { -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); box-shadow: 0 5px 15px rgba(0, 0, 0, .5); } .modal-sm { width: 300px; } } @media (min-width: 992px) { .modal-lg { width: 900px; } } .tooltip { position: absolute; z-index: 1070; display: block; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 12px; font-style: normal; font-weight: normal; line-height: 1.42857143; text-align: left; text-align: start; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-break: normal; word-spacing: normal; word-wrap: normal; white-space: normal; filter: alpha(opacity=0); opacity: 0; line-break: auto; } .tooltip.in { filter: alpha(opacity=90); opacity: .9; } .tooltip.top { padding: 5px 0; margin-top: -3px; } .tooltip.right { padding: 0 5px; margin-left: 3px; } .tooltip.bottom { padding: 5px 0; margin-top: 3px; } .tooltip.left { padding: 0 5px; margin-left: -3px; } .tooltip-inner { max-width: 200px; padding: 3px 8px; color: #fff; text-align: center; background-color: #000; border-radius: 4px; } .tooltip-arrow { position: absolute; width: 0; height: 0; border-color: transparent; border-style: solid; } .tooltip.top .tooltip-arrow { bottom: 0; left: 50%; margin-left: -5px; border-width: 5px 5px 0; border-top-color: #000; } .tooltip.top-left .tooltip-arrow { right: 5px; bottom: 0; margin-bottom: -5px; border-width: 5px 5px 0; border-top-color: #000; } .tooltip.top-right .tooltip-arrow { bottom: 0; left: 5px; margin-bottom: -5px; border-width: 5px 5px 0; border-top-color: #000; } .tooltip.right .tooltip-arrow { top: 50%; left: 0; margin-top: -5px; border-width: 5px 5px 5px 0; border-right-color: #000; } .tooltip.left .tooltip-arrow { top: 50%; right: 0; margin-top: -5px; border-width: 5px 0 5px 5px; border-left-color: #000; } .tooltip.bottom .tooltip-arrow { top: 0; left: 50%; margin-left: -5px; border-width: 0 5px 5px; border-bottom-color: #000; } .tooltip.bottom-left .tooltip-arrow { top: 0; right: 5px; margin-top: -5px; border-width: 0 5px 5px; border-bottom-color: #000; } .tooltip.bottom-right .tooltip-arrow { top: 0; left: 5px; margin-top: -5px; border-width: 0 5px 5px; border-bottom-color: #000; } .popover { position: absolute; top: 0; left: 0; z-index: 1060; display: none; max-width: 276px; padding: 1px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; font-style: normal; font-weight: normal; line-height: 1.42857143; text-align: left; text-align: start; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-break: normal; word-spacing: normal; word-wrap: normal; white-space: normal; background-color: #fff; -webkit-background-clip: padding-box; background-clip: padding-box; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, .2); border-radius: 6px; -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); box-shadow: 0 5px 10px rgba(0, 0, 0, .2); line-break: auto; } .popover.top { margin-top: -10px; } .popover.right { margin-left: 10px; } .popover.bottom { margin-top: 10px; } .popover.left { margin-left: -10px; } .popover-title { padding: 8px 14px; margin: 0; font-size: 14px; background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; border-radius: 5px 5px 0 0; } .popover-content { padding: 9px 14px; } .popover > .arrow, .popover > .arrow:after { position: absolute; display: block; width: 0; height: 0; border-color: transparent; border-style: solid; } .popover > .arrow { border-width: 11px; } .popover > .arrow:after { content: ""; border-width: 10px; } .popover.top > .arrow { bottom: -11px; left: 50%; margin-left: -11px; border-top-color: #999; border-top-color: rgba(0, 0, 0, .25); border-bottom-width: 0; } .popover.top > .arrow:after { bottom: 1px; margin-left: -10px; content: " "; border-top-color: #fff; border-bottom-width: 0; } .popover.right > .arrow { top: 50%; left: -11px; margin-top: -11px; border-right-color: #999; border-right-color: rgba(0, 0, 0, .25); border-left-width: 0; } .popover.right > .arrow:after { bottom: -10px; left: 1px; content: " "; border-right-color: #fff; border-left-width: 0; } .popover.bottom > .arrow { top: -11px; left: 50%; margin-left: -11px; border-top-width: 0; border-bottom-color: #999; border-bottom-color: rgba(0, 0, 0, .25); } .popover.bottom > .arrow:after { top: 1px; margin-left: -10px; content: " "; border-top-width: 0; border-bottom-color: #fff; } .popover.left > .arrow { top: 50%; right: -11px; margin-top: -11px; border-right-width: 0; border-left-color: #999; border-left-color: rgba(0, 0, 0, .25); } .popover.left > .arrow:after { right: 1px; bottom: -10px; content: " "; border-right-width: 0; border-left-color: #fff; } .carousel { position: relative; } .carousel-inner { position: relative; width: 100%; overflow: hidden; } .carousel-inner > .item { position: relative; display: none; -webkit-transition: .6s ease-in-out left; -o-transition: .6s ease-in-out left; transition: .6s ease-in-out left; } .carousel-inner > .item > img, .carousel-inner > .item > a > img { line-height: 1; } @media all and (transform-3d), (-webkit-transform-3d) { .carousel-inner > .item { -webkit-transition: -webkit-transform .6s ease-in-out; -o-transition: -o-transform .6s ease-in-out; transition: transform .6s ease-in-out; -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-perspective: 1000px; perspective: 1000px; } .carousel-inner > .item.next, .carousel-inner > .item.active.right { left: 0; -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); } .carousel-inner > .item.prev, .carousel-inner > .item.active.left { left: 0; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } .carousel-inner > .item.next.left, .carousel-inner > .item.prev.right, .carousel-inner > .item.active { left: 0; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .carousel-inner > .active, .carousel-inner > .next, .carousel-inner > .prev { display: block; } .carousel-inner > .active { left: 0; } .carousel-inner > .next, .carousel-inner > .prev { position: absolute; top: 0; width: 100%; } .carousel-inner > .next { left: 100%; } .carousel-inner > .prev { left: -100%; } .carousel-inner > .next.left, .carousel-inner > .prev.right { left: 0; } .carousel-inner > .active.left { left: -100%; } .carousel-inner > .active.right { left: 100%; } .carousel-control { position: absolute; top: 0; bottom: 0; left: 0; width: 15%; font-size: 20px; color: #fff; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, .6); filter: alpha(opacity=50); opacity: .5; } .carousel-control.left { background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); background-repeat: repeat-x; } .carousel-control.right { right: 0; left: auto; background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); background-repeat: repeat-x; } .carousel-control:hover, .carousel-control:focus { color: #fff; text-decoration: none; filter: alpha(opacity=90); outline: 0; opacity: .9; } .carousel-control .icon-prev, .carousel-control .icon-next, .carousel-control .glyphicon-chevron-left, .carousel-control .glyphicon-chevron-right { position: absolute; top: 50%; z-index: 5; display: inline-block; margin-top: -10px; } .carousel-control .icon-prev, .carousel-control .glyphicon-chevron-left { left: 50%; margin-left: -10px; } .carousel-control .icon-next, .carousel-control .glyphicon-chevron-right { right: 50%; margin-right: -10px; } .carousel-control .icon-prev, .carousel-control .icon-next { width: 20px; height: 20px; font-family: serif; line-height: 1; } .carousel-control .icon-prev:before { content: '\2039'; } .carousel-control .icon-next:before { content: '\203a'; } .carousel-indicators { position: absolute; bottom: 10px; left: 50%; z-index: 15; width: 60%; padding-left: 0; margin-left: -30%; text-align: center; list-style: none; } .carousel-indicators li { display: inline-block; width: 10px; height: 10px; margin: 1px; text-indent: -999px; cursor: pointer; background-color: #000 \9; background-color: rgba(0, 0, 0, 0); border: 1px solid #fff; border-radius: 10px; } .carousel-indicators .active { width: 12px; height: 12px; margin: 0; background-color: #fff; } .carousel-caption { position: absolute; right: 15%; bottom: 20px; left: 15%; z-index: 10; padding-top: 20px; padding-bottom: 20px; color: #fff; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, .6); } .carousel-caption .btn { text-shadow: none; } @media screen and (min-width: 768px) { .carousel-control .glyphicon-chevron-left, .carousel-control .glyphicon-chevron-right, .carousel-control .icon-prev, .carousel-control .icon-next { width: 30px; height: 30px; margin-top: -15px; font-size: 30px; } .carousel-control .glyphicon-chevron-left, .carousel-control .icon-prev { margin-left: -15px; } .carousel-control .glyphicon-chevron-right, .carousel-control .icon-next { margin-right: -15px; } .carousel-caption { right: 20%; left: 20%; padding-bottom: 30px; } .carousel-indicators { bottom: 20px; } } .clearfix:before, .clearfix:after, .dl-horizontal dd:before, .dl-horizontal dd:after, .container:before, .container:after, .container-fluid:before, .container-fluid:after, .row:before, .row:after, .form-horizontal .form-group:before, .form-horizontal .form-group:after, .btn-toolbar:before, .btn-toolbar:after, .btn-group-vertical > .btn-group:before, .btn-group-vertical > .btn-group:after, .nav:before, .nav:after, .navbar:before, .navbar:after, .navbar-header:before, .navbar-header:after, .navbar-collapse:before, .navbar-collapse:after, .pager:before, .pager:after, .panel-body:before, .panel-body:after, .modal-footer:before, .modal-footer:after { display: table; content: " "; } .clearfix:after, .dl-horizontal dd:after, .container:after, .container-fluid:after, .row:after, .form-horizontal .form-group:after, .btn-toolbar:after, .btn-group-vertical > .btn-group:after, .nav:after, .navbar:after, .navbar-header:after, .navbar-collapse:after, .pager:after, .panel-body:after, .modal-footer:after { clear: both; } .center-block { display: block; margin-right: auto; margin-left: auto; } .pull-right { float: right !important; } .pull-left { float: left !important; } .hide { display: none !important; } .show { display: block !important; } .invisible { visibility: hidden; } .text-hide { font: 0/0 a; color: transparent; text-shadow: none; background-color: transparent; border: 0; } .hidden { display: none !important; } .affix { position: fixed; } @-ms-viewport { width: device-width; } .visible-xs, .visible-sm, .visible-md, .visible-lg { display: none !important; } .visible-xs-block, .visible-xs-inline, .visible-xs-inline-block, .visible-sm-block, .visible-sm-inline, .visible-sm-inline-block, .visible-md-block, .visible-md-inline, .visible-md-inline-block, .visible-lg-block, .visible-lg-inline, .visible-lg-inline-block { display: none !important; } @media (max-width: 767px) { .visible-xs { display: block !important; } table.visible-xs { display: table !important; } tr.visible-xs { display: table-row !important; } th.visible-xs, td.visible-xs { display: table-cell !important; } } @media (max-width: 767px) { .visible-xs-block { display: block !important; } } @media (max-width: 767px) { .visible-xs-inline { display: inline !important; } } @media (max-width: 767px) { .visible-xs-inline-block { display: inline-block !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm { display: block !important; } table.visible-sm { display: table !important; } tr.visible-sm { display: table-row !important; } th.visible-sm, td.visible-sm { display: table-cell !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm-block { display: block !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm-inline { display: inline !important; } } @media (min-width: 768px) and (max-width: 991px) { .visible-sm-inline-block { display: inline-block !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md { display: block !important; } table.visible-md { display: table !important; } tr.visible-md { display: table-row !important; } th.visible-md, td.visible-md { display: table-cell !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md-block { display: block !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md-inline { display: inline !important; } } @media (min-width: 992px) and (max-width: 1199px) { .visible-md-inline-block { display: inline-block !important; } } @media (min-width: 1200px) { .visible-lg { display: block !important; } table.visible-lg { display: table !important; } tr.visible-lg { display: table-row !important; } th.visible-lg, td.visible-lg { display: table-cell !important; } } @media (min-width: 1200px) { .visible-lg-block { display: block !important; } } @media (min-width: 1200px) { .visible-lg-inline { display: inline !important; } } @media (min-width: 1200px) { .visible-lg-inline-block { display: inline-block !important; } } @media (max-width: 767px) { .hidden-xs { display: none !important; } } @media (min-width: 768px) and (max-width: 991px) { .hidden-sm { display: none !important; } } @media (min-width: 992px) and (max-width: 1199px) { .hidden-md { display: none !important; } } @media (min-width: 1200px) { .hidden-lg { display: none !important; } } .visible-print { display: none !important; } @media print { .visible-print { display: block !important; } table.visible-print { display: table !important; } tr.visible-print { display: table-row !important; } th.visible-print, td.visible-print { display: table-cell !important; } } .visible-print-block { display: none !important; } @media print { .visible-print-block { display: block !important; } } .visible-print-inline { display: none !important; } @media print { .visible-print-inline { display: inline !important; } } .visible-print-inline-block { display: none !important; } @media print { .visible-print-inline-block { display: inline-block !important; } } @media print { .hidden-print { display: none !important; } } /*# sourceMappingURL=bootstrap.css.map */ ================================================ FILE: static/css/common.css ================================================ /*CSS For common appearance*/ body { /*background: #F0F0F0;*/ background: url('../images/background.jpg'); } #fix-alert{ position: fixed; left: 50%; top:30%; z-index: 1; } .blog-title a:link { text-decoration: none; } .signature { padding-left: 0px; } .content { min-height: 800px; } .article-entry-sum { margin-top: 5px; } .article-entry-sum p{ font-size: 15px; color: #666; } .entry-box { background: #fff; padding: 15px; margin-bottom: 20px; border: 1px solid #e6e6e6; border-radius: 4px; } .entry-box:hover { box-shadow: 2px 2px 8px rgba(0,0,0,.2); border-color: #d6d6d6; } .article-entry-header { margin: 0; padding: 0; border: 0; font: inherit; vertical-align: baseline; } .article-entry-title { text-align: center; margin-bottom: 10px; } .article-entry-title a { text-decoration: none; } .article-entry-info { height: 35px; } .article-entry-info .base-info { float: left; } .article-entry-info .main-info { float: right; } .footer { background: #474E5D; padding: 15px 0 5px; font-size: 13px; } .footer-content { text-align: center; color: #8a6d3b; } div.pagination { width: 100%; text-align: center; padding: 0px; margin: 0px; } .footer-content a { text-decoration: none; } /*CSS For Comments(Article details page)*/ ul.comments { list-style-type: none; padding: 0px; margin: 16px 0px 0px 0px; } ul.comments li.comment:nth-child(1) { border-top: 1px solid #e0e0e0; } ul.comments li.comment { margin-left: 32px; padding: 8px; border-bottom: 1px solid #e0e0e0; } div.comment-thumbnail { position: absolute; } .comment-floor { position: absolute; margin-top: 44px; width: 40px; text-align: center; } .profile-thumbnail { position: absolute; } ul.comments div.comment-info { margin-left: 48px; min-height: 48px; } ul.comments li.comment:hover { background-color: #f0f0f0; } ul.comments .comment-date { float: right; color: #8c8c8c; } ul.comments .comment-author { font-weight: bold; color: #337ab7; } ul.comments .delete-comment{ margin: 0px 5px 0px 5px; } .comment-reply-info { color: #3f42cd; } /*CSS For Comments(admin page), use the same CSS as above*/ ul.comments .comment-handle-admin { float: right; } ul.comments .disabled-comment-admin-info { color: #2c4a35; } ul.comments div.comment-info-admin { margin-left: 68px; min-height: 48px; } div.manage-comments .select-comments-admin { margin-left: 40px; } div.manage-comments .handle-box-admin { margin-bottom: -10px; } div.manage-comments .delComments { float: right; } ul.comments .delete-comment-admin-btn { margin-left: 5px; margin-right: 5px; } ul.comments .disabled-comment-admin-btn { margin-left: 5px; } /*CSS For submit comment*/ .comment-submit { margin-left: 25px; } /*CSS For floatButton*/ .floatButton { position: fixed; top: 50%; right: 0; z-index: 9999999; } /*CSS For submit article*/ .submit-article-title { width: 450px; } .submit-article-editor { margin: 10px 0px 10px 0px; } .submit-article-summary { width: 500px; } .submit-article-button { text-align: center; } /*.submit-article-button {*/ /*margin: auto;*/ /*}*/ /*CSS For edit article*/ .article-edit { text-align: right; } /*CSS For manage articles*/ .articles-list { margin-left: 30px; margin-right: 30px; } .manage-articles th { text-align: center; } .manage-articles td { text-align: center; } .manage-articles .table-header { background-color: #779ecb } .manage-articles .table-checkbox-or-left { text-align: left; } .manage-articles .delete-article { color: #c9302c; } .manage-articles .list-handle { padding-bottom: 25px; } /*CSS For manage articleTypes*/ .manage-articleTypes .articleTypes-box { margin-left: 30px; margin-right: 30px; } .manage-articleTypes th { text-align: center; } .manage-articleTypes td { text-align: center; } .manage-articleTypes .table-header { background-color: #779ecb; /*background-color: #337ab7;*/ } .manage-articleTypes .table-checkbox-or-left { text-align: left; } .manage-articleTypes .delete-articleType { color: #c9302c; } .manage-articleTypes .add-articleType-btn { float: right; } /*CSS For manage articleTypes nav*/ .manage-articleTypes-nav .articleTypes-nav-box { margin-left: 30px; margin-right: 30px; } .manage-articleTypes-nav th { text-align: center; } .manage-articleTypes-nav td { text-align: center; } .manage-articleTypes-nav .table-header { background-color: #779ecb; } .manage-articleTypes-nav .delete-articleType-nav { color: #c9302c; } .manage-articleTypes-nav .add-articleType-nav-btn { float: right; } .sort-up { color: #449d44; } .sort-down { color: #337ab7; } /*CSS For custom blog info*/ /*.custom-blog .blog-info-box {*/ /*margin-left: 30px;*/ /*}*/ .custom-blog .info-header { color: #337ab7; } /*CSS For custom blog plugin*/ .custom-blog-plugin .blog-plugin-box { margin-left: 30px; margin-right: 30px; } .custom-blog-plugin th { text-align: center; } .custom-blog-plugin td { text-align: center; } .custom-blog-plugin .table-header { background-color: #779ecb; } .custom-blog-plugin .delete-blog-plugin { color: #c9302c; } .custom-blog-plugin .add-blog-plugin-btn { float: right; } /*CSS For account*/ .account { min-height: 250px; } .account .profile-header { margin-left: 135px; } .account .username { color: #337ab7; } .account .email { color: #337ab7; } /*CSS For login page*/ .login-page { margin-top: 200px; } .dropdown-menu .divider{ margin:0; } .dropdown-menu{ padding:0; } .dropdown-menu > li > a{ padding:8px 20px; } ================================================ FILE: static/css/prism.css ================================================ /* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+abap+actionscript+apacheconf+apl+applescript+asciidoc+aspnet+autoit+autohotkey+bash+basic+batch+c+brainfuck+bison+csharp+cpp+coffeescript+ruby+css-extras+d+dart+diff+docker+eiffel+elixir+erlang+fsharp+fortran+gherkin+git+glsl+go+groovy+haml+handlebars+haskell+haxe+http+icon+inform7+ini+j+jade+java+json+julia+keyman+kotlin+latex+less+lolcode+lua+makefile+markdown+matlab+mel+mizar+monkey+nasm+nginx+nim+nix+nsis+objectivec+ocaml+oz+parigp+parser+pascal+perl+php+php-extras+powershell+processing+prolog+puppet+pure+python+q+qore+r+jsx+rest+rip+roboconf+crystal+rust+sas+sass+scss+scala+scheme+smalltalk+smarty+sql+stylus+swift+tcl+textile+twig+typescript+verilog+vhdl+vim+wiki+yaml */ /** * prism.js default theme for JavaScript, CSS and HTML * Based on dabblet (http://dabblet.com) * @author Lea Verou */ code[class*="language-"], pre[class*="language-"] { color: black; background: none; text-shadow: 0 1px white; font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; direction: ltr; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; word-wrap: normal; line-height: 1.5; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { text-shadow: none; background: #b3d4fc; } pre[class*="language-"]::selection, pre[class*="language-"] ::selection, code[class*="language-"]::selection, code[class*="language-"] ::selection { text-shadow: none; background: #b3d4fc; } @media print { code[class*="language-"], pre[class*="language-"] { text-shadow: none; } } /* Code blocks */ pre[class*="language-"] { padding: 1em; margin: .5em 0; overflow: auto; } :not(pre) > code[class*="language-"], pre[class*="language-"] { background: #f5f2f0; } /* Inline code */ :not(pre) > code[class*="language-"] { padding: .1em; border-radius: .3em; white-space: normal; } .token.comment, .token.prolog, .token.doctype, .token.cdata { color: slategray; } .token.punctuation { color: #999; } .namespace { opacity: .7; } .token.property, .token.tag, .token.boolean, .token.number, .token.constant, .token.symbol, .token.deleted { color: #905; } .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #690; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #a67f59; background: hsla(0, 0%, 100%, .5); } .token.atrule, .token.attr-value, .token.keyword { color: #07a; } .token.function { color: #DD4A68; } .token.regex, .token.important, .token.variable { color: #e90; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.entity { cursor: help; } ================================================ FILE: static/js/admin.js ================================================ //JS For manage-articles when select articles or select comments $(document).ready(function () { $('#select-all').click(function () { if ($(this).prop('checked')) { $('.op_check').prop('checked', true); } else { $('.op_check').prop('checked', false); } }); }); //JS For submit article id to delete article function delCfm(delLink) { $('#cfmClick').click(function () { var _xsrf = getCookie("_xsrf"); $.post(delLink, {_xsrf:_xsrf}, function () { location.reload(); }); }); $('#delCfmModel').modal(); } //JS For select articles to delete $(document).ready(function () { $('#delArtsCfm').click(function(){ $('#delArticlesForm').submit(); }); $('#delArticles').click(function () { if ($('.op_check').filter(':checked').size() > 0) { var articleIds = []; $('.op_check:checked').each(function(){ articleIds.push($(this).val()); }); var articleIdsJson = JSON.stringify(articleIds); $('#articleIds').val(articleIdsJson); $('#delArtsCfmModel').modal(); } else { $('#selArtsCfmModel').modal(); } }); }); //JS For confirm to delete a comment in articleDetails page function delCommentCfm(url) { $('#delCommentCfmClick').click(function(){ window.location.href = url; }); $('#delCommentCfmModel').modal(); } $(document).ready(function(){ $('#delComments').click(function(){ if($('.op_check_com').filter(':checked').size() > 0) { var commentIds = []; $('.op_check_com:checked').each(function(){ commentIds.push($(this).val()); }); var commentIdsJson = JSON.stringify(commentIds); $('#commentIds').val(commentIdsJson); $('#delComsCfm').click(function() { $('#delCommentsForm').submit(); }); $('#delComsCfmModel').modal(); } else { $('#selComsCfmModal').modal(); } }); }); //JS For reply to a comment in manage-comments page function pop_commentForm(followId, articleId) { $('#follow').val(followId); $('#article').val(articleId); $('#commentFormModel').modal(); } //JS For add articleType $(document).ready(function() { $('.add-articleType-btn').click(function() { $('#addArticleTypeFormModel').modal(); }); }); //JS For confirm to delete an articleType function delArticleTypeCfm(url) { $('#delArticleTypeCfmClick').click(function(){ window.location.href = url; }); $('#delArticleTypeCfmModel').modal(); } //JS For edit articleType to get its info function get_articleType_info(url, id, name, is_hide, introduction, menu_id) { $('#editName').val(name); $('#editSetting_hide').val(is_hide?'true':'false'); $('#editIntroduction').val(introduction); $('#editMenus').val(menu_id?menu_id:-1); $('#articleType_id').val(id); $('#editArticleTypeForm').attr('action', url) $('#ModalTitle').text('修改博文分类:' + name); if (name == '未分类') { $('#editName').prop('readonly', true); $('#editIntroduction').prop('readonly', true); } else { $('#editName').prop('readonly', false); $('#editIntroduction').prop('readonly', false); } $('#editArticleTypeFormModel').modal(); } //JS For add articleType $(document).ready(function() { $('.add-articleType-nav-btn').click(function() { $('#addArticleTypeNavFormModel').modal(); }); }); //JS For edit articleTypeNav to get its info function get_articleTypeNav_info(url, id, name) { $('#editNavName').val(name); $('#nav_id').val(id); $('#editArticleTypeNavForm').attr('action', url) $('#NavModalTitle').text('修改分类导航:' + name); $('#editArticleTypeNavFormModel').modal(); } //JS For confirm to delete an articleType nav function delArticleTypeNavCfm(url) { $('#delArticleTypeNavCfmClick').click(function(){ window.location.href = url; }); $('#delArticleTypeNavCfmModel').modal(); } //JS For editing blog info function get_blog_info() { $('#editBlogInfoFormModal').modal(); } //JS For confirm to delete a plugin function delPluginCfm(url) { $('#delPluginCfmClick').click(function () { location.href = url }); $('#delPluginCfmModal').modal(); } //JS For change password function changePassword() { $('#changePasswordFormModal').modal(); } //JS For edit user info function editUserInfo() { $('#editUserInfoFormModal').modal(); } //js for checkDoublePassword function checkChangePasswordForm() { var password = $('#password').val(); var password2 = $('#password2').val(); if (password != password2) { $('#group_password2').addClass('has-error'); $('#password2_err').show(); return false; } else { $('#changePasswordForm').submit(); return true; } } function update_comment(url) { var _xsrf = getCookie("_xsrf"); $.post(url, {_xsrf:_xsrf}, function (data) { location.reload(); }); } function delCommentCfm(url) { $('#delCommentCfmClick').click(function(){ update_comment(url); }); $('#delCommentCfmModel').modal(); } function replyComment(action, reply_to_id, reply_to_floor) { $('#commentForm').attr('action', action); $('#commentForm input[name=reply_to_id]').val(reply_to_id); $('#commentForm input[name=reply_to_floor]').val(reply_to_floor); $('#commentFormModel').modal(); } ================================================ FILE: static/js/articleDetail.js ================================================ function update_disable(url) { var _xsrf = getCookie("_xsrf"); $.post(url, {_xsrf:_xsrf}, function (data) { location.reload(); }); } function delete_comment(url) { var _xsrf = getCookie("_xsrf"); $.post(url, {_xsrf:_xsrf}, function (data) { location.reload(); }); } function delCommentCfm(url) { $('#delCommentCfmClick').click(function(){ delete_comment(url); }); $('#delCommentCfmModel').modal(); } function go_to_reply(comment_type, reply_to_id, reply_to_floor) { $('#reply-dialog-box').remove(); $('#submit-comment-form').prepend('
' + ''+ ''+ ''+ '' + '回复给' + reply_to_floor +'
'); $('html, body').animate({scrollTop:$('#submit-comment').offset().top}); } //Reset the follow value when refresh page window.onload = function(){ var content = $('.article-content').text(); $('.article-content').html(markdown.toHTML(content)); $('.article-loading').hide(); $('.article-content').show(); codeHighLight(); var scrollName = location.hash; if(scrollName) { $("body,html").animate({scrollTop: $(scrollName).offset().top}, "fast"); } } ================================================ FILE: static/js/bootstrap.js ================================================ /*! * Bootstrap v3.3.5 (http://getbootstrap.com) * Copyright 2011-2015 Twitter, Inc. * Licensed under the MIT license */ if (typeof jQuery === 'undefined') { throw new Error('Bootstrap\'s JavaScript requires jQuery') } +function ($) { 'use strict'; var version = $.fn.jquery.split(' ')[0].split('.') if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) { throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher') } }(jQuery); /* ======================================================================== * Bootstrap: transition.js v3.3.5 * http://getbootstrap.com/javascript/#transitions * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) // ============================================================ function transitionEnd() { var el = document.createElement('bootstrap') var transEndEventNames = { WebkitTransition : 'webkitTransitionEnd', MozTransition : 'transitionend', OTransition : 'oTransitionEnd otransitionend', transition : 'transitionend' } for (var name in transEndEventNames) { if (el.style[name] !== undefined) { return { end: transEndEventNames[name] } } } return false // explicit for ie8 ( ._.) } // http://blog.alexmaccaw.com/css-transitions $.fn.emulateTransitionEnd = function (duration) { var called = false var $el = this $(this).one('bsTransitionEnd', function () { called = true }) var callback = function () { if (!called) $($el).trigger($.support.transition.end) } setTimeout(callback, duration) return this } $(function () { $.support.transition = transitionEnd() if (!$.support.transition) return $.event.special.bsTransitionEnd = { bindType: $.support.transition.end, delegateType: $.support.transition.end, handle: function (e) { if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) } } }) }(jQuery); /* ======================================================================== * Bootstrap: alert.js v3.3.5 * http://getbootstrap.com/javascript/#alerts * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // ALERT CLASS DEFINITION // ====================== var dismiss = '[data-dismiss="alert"]' var Alert = function (el) { $(el).on('click', dismiss, this.close) } Alert.VERSION = '3.3.5' Alert.TRANSITION_DURATION = 150 Alert.prototype.close = function (e) { var $this = $(this) var selector = $this.attr('data-target') if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } var $parent = $(selector) if (e) e.preventDefault() if (!$parent.length) { $parent = $this.closest('.alert') } $parent.trigger(e = $.Event('close.bs.alert')) if (e.isDefaultPrevented()) return $parent.removeClass('in') function removeElement() { // detach from parent, fire event then clean up data $parent.detach().trigger('closed.bs.alert').remove() } $.support.transition && $parent.hasClass('fade') ? $parent .one('bsTransitionEnd', removeElement) .emulateTransitionEnd(Alert.TRANSITION_DURATION) : removeElement() } // ALERT PLUGIN DEFINITION // ======================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.alert') if (!data) $this.data('bs.alert', (data = new Alert(this))) if (typeof option == 'string') data[option].call($this) }) } var old = $.fn.alert $.fn.alert = Plugin $.fn.alert.Constructor = Alert // ALERT NO CONFLICT // ================= $.fn.alert.noConflict = function () { $.fn.alert = old return this } // ALERT DATA-API // ============== $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) }(jQuery); /* ======================================================================== * Bootstrap: button.js v3.3.5 * http://getbootstrap.com/javascript/#buttons * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // BUTTON PUBLIC CLASS DEFINITION // ============================== var Button = function (element, options) { this.$element = $(element) this.options = $.extend({}, Button.DEFAULTS, options) this.isLoading = false } Button.VERSION = '3.3.5' Button.DEFAULTS = { loadingText: 'loading...' } Button.prototype.setState = function (state) { var d = 'disabled' var $el = this.$element var val = $el.is('input') ? 'val' : 'html' var data = $el.data() state += 'Text' if (data.resetText == null) $el.data('resetText', $el[val]()) // push to event loop to allow forms to submit setTimeout($.proxy(function () { $el[val](data[state] == null ? this.options[state] : data[state]) if (state == 'loadingText') { this.isLoading = true $el.addClass(d).attr(d, d) } else if (this.isLoading) { this.isLoading = false $el.removeClass(d).removeAttr(d) } }, this), 0) } Button.prototype.toggle = function () { var changed = true var $parent = this.$element.closest('[data-toggle="buttons"]') if ($parent.length) { var $input = this.$element.find('input') if ($input.prop('type') == 'radio') { if ($input.prop('checked')) changed = false $parent.find('.active').removeClass('active') this.$element.addClass('active') } else if ($input.prop('type') == 'checkbox') { if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false this.$element.toggleClass('active') } $input.prop('checked', this.$element.hasClass('active')) if (changed) $input.trigger('change') } else { this.$element.attr('aria-pressed', !this.$element.hasClass('active')) this.$element.toggleClass('active') } } // BUTTON PLUGIN DEFINITION // ======================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.button') var options = typeof option == 'object' && option if (!data) $this.data('bs.button', (data = new Button(this, options))) if (option == 'toggle') data.toggle() else if (option) data.setState(option) }) } var old = $.fn.button $.fn.button = Plugin $.fn.button.Constructor = Button // BUTTON NO CONFLICT // ================== $.fn.button.noConflict = function () { $.fn.button = old return this } // BUTTON DATA-API // =============== $(document) .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { var $btn = $(e.target) if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') Plugin.call($btn, 'toggle') if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() }) .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) }) }(jQuery); /* ======================================================================== * Bootstrap: carousel.js v3.3.5 * http://getbootstrap.com/javascript/#carousel * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // CAROUSEL CLASS DEFINITION // ========================= var Carousel = function (element, options) { this.$element = $(element) this.$indicators = this.$element.find('.carousel-indicators') this.options = options this.paused = null this.sliding = null this.interval = null this.$active = null this.$items = null this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) } Carousel.VERSION = '3.3.5' Carousel.TRANSITION_DURATION = 600 Carousel.DEFAULTS = { interval: 5000, pause: 'hover', wrap: true, keyboard: true } Carousel.prototype.keydown = function (e) { if (/input|textarea/i.test(e.target.tagName)) return switch (e.which) { case 37: this.prev(); break case 39: this.next(); break default: return } e.preventDefault() } Carousel.prototype.cycle = function (e) { e || (this.paused = false) this.interval && clearInterval(this.interval) this.options.interval && !this.paused && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) return this } Carousel.prototype.getItemIndex = function (item) { this.$items = item.parent().children('.item') return this.$items.index(item || this.$active) } Carousel.prototype.getItemForDirection = function (direction, active) { var activeIndex = this.getItemIndex(active) var willWrap = (direction == 'prev' && activeIndex === 0) || (direction == 'next' && activeIndex == (this.$items.length - 1)) if (willWrap && !this.options.wrap) return active var delta = direction == 'prev' ? -1 : 1 var itemIndex = (activeIndex + delta) % this.$items.length return this.$items.eq(itemIndex) } Carousel.prototype.to = function (pos) { var that = this var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) if (pos > (this.$items.length - 1) || pos < 0) return if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" if (activeIndex == pos) return this.pause().cycle() return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) } Carousel.prototype.pause = function (e) { e || (this.paused = true) if (this.$element.find('.next, .prev').length && $.support.transition) { this.$element.trigger($.support.transition.end) this.cycle(true) } this.interval = clearInterval(this.interval) return this } Carousel.prototype.next = function () { if (this.sliding) return return this.slide('next') } Carousel.prototype.prev = function () { if (this.sliding) return return this.slide('prev') } Carousel.prototype.slide = function (type, next) { var $active = this.$element.find('.item.active') var $next = next || this.getItemForDirection(type, $active) var isCycling = this.interval var direction = type == 'next' ? 'left' : 'right' var that = this if ($next.hasClass('active')) return (this.sliding = false) var relatedTarget = $next[0] var slideEvent = $.Event('slide.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) this.$element.trigger(slideEvent) if (slideEvent.isDefaultPrevented()) return this.sliding = true isCycling && this.pause() if (this.$indicators.length) { this.$indicators.find('.active').removeClass('active') var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) $nextIndicator && $nextIndicator.addClass('active') } var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" if ($.support.transition && this.$element.hasClass('slide')) { $next.addClass(type) $next[0].offsetWidth // force reflow $active.addClass(direction) $next.addClass(direction) $active .one('bsTransitionEnd', function () { $next.removeClass([type, direction].join(' ')).addClass('active') $active.removeClass(['active', direction].join(' ')) that.sliding = false setTimeout(function () { that.$element.trigger(slidEvent) }, 0) }) .emulateTransitionEnd(Carousel.TRANSITION_DURATION) } else { $active.removeClass('active') $next.addClass('active') this.sliding = false this.$element.trigger(slidEvent) } isCycling && this.cycle() return this } // CAROUSEL PLUGIN DEFINITION // ========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.carousel') var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) var action = typeof option == 'string' ? option : options.slide if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) if (typeof option == 'number') data.to(option) else if (action) data[action]() else if (options.interval) data.pause().cycle() }) } var old = $.fn.carousel $.fn.carousel = Plugin $.fn.carousel.Constructor = Carousel // CAROUSEL NO CONFLICT // ==================== $.fn.carousel.noConflict = function () { $.fn.carousel = old return this } // CAROUSEL DATA-API // ================= var clickHandler = function (e) { var href var $this = $(this) var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 if (!$target.hasClass('carousel')) return var options = $.extend({}, $target.data(), $this.data()) var slideIndex = $this.attr('data-slide-to') if (slideIndex) options.interval = false Plugin.call($target, options) if (slideIndex) { $target.data('bs.carousel').to(slideIndex) } e.preventDefault() } $(document) .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) $(window).on('load', function () { $('[data-ride="carousel"]').each(function () { var $carousel = $(this) Plugin.call($carousel, $carousel.data()) }) }) }(jQuery); /* ======================================================================== * Bootstrap: collapse.js v3.3.5 * http://getbootstrap.com/javascript/#collapse * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // COLLAPSE PUBLIC CLASS DEFINITION // ================================ var Collapse = function (element, options) { this.$element = $(element) this.options = $.extend({}, Collapse.DEFAULTS, options) this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + '[data-toggle="collapse"][data-target="#' + element.id + '"]') this.transitioning = null if (this.options.parent) { this.$parent = this.getParent() } else { this.addAriaAndCollapsedClass(this.$element, this.$trigger) } if (this.options.toggle) this.toggle() } Collapse.VERSION = '3.3.5' Collapse.TRANSITION_DURATION = 350 Collapse.DEFAULTS = { toggle: true } Collapse.prototype.dimension = function () { var hasWidth = this.$element.hasClass('width') return hasWidth ? 'width' : 'height' } Collapse.prototype.show = function () { if (this.transitioning || this.$element.hasClass('in')) return var activesData var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') if (actives && actives.length) { activesData = actives.data('bs.collapse') if (activesData && activesData.transitioning) return } var startEvent = $.Event('show.bs.collapse') this.$element.trigger(startEvent) if (startEvent.isDefaultPrevented()) return if (actives && actives.length) { Plugin.call(actives, 'hide') activesData || actives.data('bs.collapse', null) } var dimension = this.dimension() this.$element .removeClass('collapse') .addClass('collapsing')[dimension](0) .attr('aria-expanded', true) this.$trigger .removeClass('collapsed') .attr('aria-expanded', true) this.transitioning = 1 var complete = function () { this.$element .removeClass('collapsing') .addClass('collapse in')[dimension]('') this.transitioning = 0 this.$element .trigger('shown.bs.collapse') } if (!$.support.transition) return complete.call(this) var scrollSize = $.camelCase(['scroll', dimension].join('-')) this.$element .one('bsTransitionEnd', $.proxy(complete, this)) .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) } Collapse.prototype.hide = function () { if (this.transitioning || !this.$element.hasClass('in')) return var startEvent = $.Event('hide.bs.collapse') this.$element.trigger(startEvent) if (startEvent.isDefaultPrevented()) return var dimension = this.dimension() this.$element[dimension](this.$element[dimension]())[0].offsetHeight this.$element .addClass('collapsing') .removeClass('collapse in') .attr('aria-expanded', false) this.$trigger .addClass('collapsed') .attr('aria-expanded', false) this.transitioning = 1 var complete = function () { this.transitioning = 0 this.$element .removeClass('collapsing') .addClass('collapse') .trigger('hidden.bs.collapse') } if (!$.support.transition) return complete.call(this) this.$element [dimension](0) .one('bsTransitionEnd', $.proxy(complete, this)) .emulateTransitionEnd(Collapse.TRANSITION_DURATION) } Collapse.prototype.toggle = function () { this[this.$element.hasClass('in') ? 'hide' : 'show']() } Collapse.prototype.getParent = function () { return $(this.options.parent) .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') .each($.proxy(function (i, element) { var $element = $(element) this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) }, this)) .end() } Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { var isOpen = $element.hasClass('in') $element.attr('aria-expanded', isOpen) $trigger .toggleClass('collapsed', !isOpen) .attr('aria-expanded', isOpen) } function getTargetFromTrigger($trigger) { var href var target = $trigger.attr('data-target') || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 return $(target) } // COLLAPSE PLUGIN DEFINITION // ========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.collapse') var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.collapse $.fn.collapse = Plugin $.fn.collapse.Constructor = Collapse // COLLAPSE NO CONFLICT // ==================== $.fn.collapse.noConflict = function () { $.fn.collapse = old return this } // COLLAPSE DATA-API // ================= $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { var $this = $(this) if (!$this.attr('data-target')) e.preventDefault() var $target = getTargetFromTrigger($this) var data = $target.data('bs.collapse') var option = data ? 'toggle' : $this.data() Plugin.call($target, option) }) }(jQuery); /* ======================================================================== * Bootstrap: dropdown.js v3.3.5 * http://getbootstrap.com/javascript/#dropdowns * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // DROPDOWN CLASS DEFINITION // ========================= var backdrop = '.dropdown-backdrop' var toggle = '[data-toggle="dropdown"]' var Dropdown = function (element) { $(element).on('click.bs.dropdown', this.toggle) } Dropdown.VERSION = '3.3.5' function getParent($this) { var selector = $this.attr('data-target') if (!selector) { selector = $this.attr('href') selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } var $parent = selector && $(selector) return $parent && $parent.length ? $parent : $this.parent() } function clearMenus(e) { if (e && e.which === 3) return $(backdrop).remove() $(toggle).each(function () { var $this = $(this) var $parent = getParent($this) var relatedTarget = { relatedTarget: this } if (!$parent.hasClass('open')) return if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) if (e.isDefaultPrevented()) return $this.attr('aria-expanded', 'false') $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) }) } Dropdown.prototype.toggle = function (e) { var $this = $(this) if ($this.is('.disabled, :disabled')) return var $parent = getParent($this) var isActive = $parent.hasClass('open') clearMenus() if (!isActive) { if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { // if mobile we use a backdrop because click events don't delegate $(document.createElement('div')) .addClass('dropdown-backdrop') .insertAfter($(this)) .on('click', clearMenus) } var relatedTarget = { relatedTarget: this } $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) if (e.isDefaultPrevented()) return $this .trigger('focus') .attr('aria-expanded', 'true') $parent .toggleClass('open') .trigger('shown.bs.dropdown', relatedTarget) } return false } Dropdown.prototype.keydown = function (e) { if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return var $this = $(this) e.preventDefault() e.stopPropagation() if ($this.is('.disabled, :disabled')) return var $parent = getParent($this) var isActive = $parent.hasClass('open') if (!isActive && e.which != 27 || isActive && e.which == 27) { if (e.which == 27) $parent.find(toggle).trigger('focus') return $this.trigger('click') } var desc = ' li:not(.disabled):visible a' var $items = $parent.find('.dropdown-menu' + desc) if (!$items.length) return var index = $items.index(e.target) if (e.which == 38 && index > 0) index-- // up if (e.which == 40 && index < $items.length - 1) index++ // down if (!~index) index = 0 $items.eq(index).trigger('focus') } // DROPDOWN PLUGIN DEFINITION // ========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.dropdown') if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) if (typeof option == 'string') data[option].call($this) }) } var old = $.fn.dropdown $.fn.dropdown = Plugin $.fn.dropdown.Constructor = Dropdown // DROPDOWN NO CONFLICT // ==================== $.fn.dropdown.noConflict = function () { $.fn.dropdown = old return this } // APPLY TO STANDARD DROPDOWN ELEMENTS // =================================== $(document) .on('click.bs.dropdown.data-api', clearMenus) .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) }(jQuery); /* ======================================================================== * Bootstrap: modal.js v3.3.5 * http://getbootstrap.com/javascript/#modals * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // MODAL CLASS DEFINITION // ====================== var Modal = function (element, options) { this.options = options this.$body = $(document.body) this.$element = $(element) this.$dialog = this.$element.find('.modal-dialog') this.$backdrop = null this.isShown = null this.originalBodyPad = null this.scrollbarWidth = 0 this.ignoreBackdropClick = false if (this.options.remote) { this.$element .find('.modal-content') .load(this.options.remote, $.proxy(function () { this.$element.trigger('loaded.bs.modal') }, this)) } } Modal.VERSION = '3.3.5' Modal.TRANSITION_DURATION = 300 Modal.BACKDROP_TRANSITION_DURATION = 150 Modal.DEFAULTS = { backdrop: true, keyboard: true, show: true } Modal.prototype.toggle = function (_relatedTarget) { return this.isShown ? this.hide() : this.show(_relatedTarget) } Modal.prototype.show = function (_relatedTarget) { var that = this var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) this.$element.trigger(e) if (this.isShown || e.isDefaultPrevented()) return this.isShown = true this.checkScrollbar() this.setScrollbar() this.$body.addClass('modal-open') this.escape() this.resize() this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) this.$dialog.on('mousedown.dismiss.bs.modal', function () { that.$element.one('mouseup.dismiss.bs.modal', function (e) { if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true }) }) this.backdrop(function () { var transition = $.support.transition && that.$element.hasClass('fade') if (!that.$element.parent().length) { that.$element.appendTo(that.$body) // don't move modals dom position } that.$element .show() .scrollTop(0) that.adjustDialog() if (transition) { that.$element[0].offsetWidth // force reflow } that.$element.addClass('in') that.enforceFocus() var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) transition ? that.$dialog // wait for modal to slide in .one('bsTransitionEnd', function () { that.$element.trigger('focus').trigger(e) }) .emulateTransitionEnd(Modal.TRANSITION_DURATION) : that.$element.trigger('focus').trigger(e) }) } Modal.prototype.hide = function (e) { if (e) e.preventDefault() e = $.Event('hide.bs.modal') this.$element.trigger(e) if (!this.isShown || e.isDefaultPrevented()) return this.isShown = false this.escape() this.resize() $(document).off('focusin.bs.modal') this.$element .removeClass('in') .off('click.dismiss.bs.modal') .off('mouseup.dismiss.bs.modal') this.$dialog.off('mousedown.dismiss.bs.modal') $.support.transition && this.$element.hasClass('fade') ? this.$element .one('bsTransitionEnd', $.proxy(this.hideModal, this)) .emulateTransitionEnd(Modal.TRANSITION_DURATION) : this.hideModal() } Modal.prototype.enforceFocus = function () { $(document) .off('focusin.bs.modal') // guard against infinite focus loop .on('focusin.bs.modal', $.proxy(function (e) { if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { this.$element.trigger('focus') } }, this)) } Modal.prototype.escape = function () { if (this.isShown && this.options.keyboard) { this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { e.which == 27 && this.hide() }, this)) } else if (!this.isShown) { this.$element.off('keydown.dismiss.bs.modal') } } Modal.prototype.resize = function () { if (this.isShown) { $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) } else { $(window).off('resize.bs.modal') } } Modal.prototype.hideModal = function () { var that = this this.$element.hide() this.backdrop(function () { that.$body.removeClass('modal-open') that.resetAdjustments() that.resetScrollbar() that.$element.trigger('hidden.bs.modal') }) } Modal.prototype.removeBackdrop = function () { this.$backdrop && this.$backdrop.remove() this.$backdrop = null } Modal.prototype.backdrop = function (callback) { var that = this var animate = this.$element.hasClass('fade') ? 'fade' : '' if (this.isShown && this.options.backdrop) { var doAnimate = $.support.transition && animate this.$backdrop = $(document.createElement('div')) .addClass('modal-backdrop ' + animate) .appendTo(this.$body) this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { if (this.ignoreBackdropClick) { this.ignoreBackdropClick = false return } if (e.target !== e.currentTarget) return this.options.backdrop == 'static' ? this.$element[0].focus() : this.hide() }, this)) if (doAnimate) this.$backdrop[0].offsetWidth // force reflow this.$backdrop.addClass('in') if (!callback) return doAnimate ? this.$backdrop .one('bsTransitionEnd', callback) .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : callback() } else if (!this.isShown && this.$backdrop) { this.$backdrop.removeClass('in') var callbackRemove = function () { that.removeBackdrop() callback && callback() } $.support.transition && this.$element.hasClass('fade') ? this.$backdrop .one('bsTransitionEnd', callbackRemove) .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : callbackRemove() } else if (callback) { callback() } } // these following methods are used to handle overflowing modals Modal.prototype.handleUpdate = function () { this.adjustDialog() } Modal.prototype.adjustDialog = function () { var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight this.$element.css({ paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' }) } Modal.prototype.resetAdjustments = function () { this.$element.css({ paddingLeft: '', paddingRight: '' }) } Modal.prototype.checkScrollbar = function () { var fullWindowWidth = window.innerWidth if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 var documentElementRect = document.documentElement.getBoundingClientRect() fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) } this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth this.scrollbarWidth = this.measureScrollbar() } Modal.prototype.setScrollbar = function () { var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) this.originalBodyPad = document.body.style.paddingRight || '' if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) } Modal.prototype.resetScrollbar = function () { this.$body.css('padding-right', this.originalBodyPad) } Modal.prototype.measureScrollbar = function () { // thx walsh var scrollDiv = document.createElement('div') scrollDiv.className = 'modal-scrollbar-measure' this.$body.append(scrollDiv) var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth this.$body[0].removeChild(scrollDiv) return scrollbarWidth } // MODAL PLUGIN DEFINITION // ======================= function Plugin(option, _relatedTarget) { return this.each(function () { var $this = $(this) var data = $this.data('bs.modal') var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) if (!data) $this.data('bs.modal', (data = new Modal(this, options))) if (typeof option == 'string') data[option](_relatedTarget) else if (options.show) data.show(_relatedTarget) }) } var old = $.fn.modal $.fn.modal = Plugin $.fn.modal.Constructor = Modal // MODAL NO CONFLICT // ================= $.fn.modal.noConflict = function () { $.fn.modal = old return this } // MODAL DATA-API // ============== $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { var $this = $(this) var href = $this.attr('href') var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) if ($this.is('a')) e.preventDefault() $target.one('show.bs.modal', function (showEvent) { if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown $target.one('hidden.bs.modal', function () { $this.is(':visible') && $this.trigger('focus') }) }) Plugin.call($target, option, this) }) }(jQuery); /* ======================================================================== * Bootstrap: tooltip.js v3.3.5 * http://getbootstrap.com/javascript/#tooltip * Inspired by the original jQuery.tipsy by Jason Frame * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // TOOLTIP PUBLIC CLASS DEFINITION // =============================== var Tooltip = function (element, options) { this.type = null this.options = null this.enabled = null this.timeout = null this.hoverState = null this.$element = null this.inState = null this.init('tooltip', element, options) } Tooltip.VERSION = '3.3.5' Tooltip.TRANSITION_DURATION = 150 Tooltip.DEFAULTS = { animation: true, placement: 'top', selector: false, template: '', trigger: 'hover focus', title: '', delay: 0, html: false, container: false, viewport: { selector: 'body', padding: 0 } } Tooltip.prototype.init = function (type, element, options) { this.enabled = true this.type = type this.$element = $(element) this.options = this.getOptions(options) this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) this.inState = { click: false, hover: false, focus: false } if (this.$element[0] instanceof document.constructor && !this.options.selector) { throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') } var triggers = this.options.trigger.split(' ') for (var i = triggers.length; i--;) { var trigger = triggers[i] if (trigger == 'click') { this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) } else if (trigger != 'manual') { var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) } } this.options.selector ? (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : this.fixTitle() } Tooltip.prototype.getDefaults = function () { return Tooltip.DEFAULTS } Tooltip.prototype.getOptions = function (options) { options = $.extend({}, this.getDefaults(), this.$element.data(), options) if (options.delay && typeof options.delay == 'number') { options.delay = { show: options.delay, hide: options.delay } } return options } Tooltip.prototype.getDelegateOptions = function () { var options = {} var defaults = this.getDefaults() this._options && $.each(this._options, function (key, value) { if (defaults[key] != value) options[key] = value }) return options } Tooltip.prototype.enter = function (obj) { var self = obj instanceof this.constructor ? obj : $(obj.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) $(obj.currentTarget).data('bs.' + this.type, self) } if (obj instanceof $.Event) { self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true } if (self.tip().hasClass('in') || self.hoverState == 'in') { self.hoverState = 'in' return } clearTimeout(self.timeout) self.hoverState = 'in' if (!self.options.delay || !self.options.delay.show) return self.show() self.timeout = setTimeout(function () { if (self.hoverState == 'in') self.show() }, self.options.delay.show) } Tooltip.prototype.isInStateTrue = function () { for (var key in this.inState) { if (this.inState[key]) return true } return false } Tooltip.prototype.leave = function (obj) { var self = obj instanceof this.constructor ? obj : $(obj.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) $(obj.currentTarget).data('bs.' + this.type, self) } if (obj instanceof $.Event) { self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false } if (self.isInStateTrue()) return clearTimeout(self.timeout) self.hoverState = 'out' if (!self.options.delay || !self.options.delay.hide) return self.hide() self.timeout = setTimeout(function () { if (self.hoverState == 'out') self.hide() }, self.options.delay.hide) } Tooltip.prototype.show = function () { var e = $.Event('show.bs.' + this.type) if (this.hasContent() && this.enabled) { this.$element.trigger(e) var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) if (e.isDefaultPrevented() || !inDom) return var that = this var $tip = this.tip() var tipId = this.getUID(this.type) this.setContent() $tip.attr('id', tipId) this.$element.attr('aria-describedby', tipId) if (this.options.animation) $tip.addClass('fade') var placement = typeof this.options.placement == 'function' ? this.options.placement.call(this, $tip[0], this.$element[0]) : this.options.placement var autoToken = /\s?auto?\s?/i var autoPlace = autoToken.test(placement) if (autoPlace) placement = placement.replace(autoToken, '') || 'top' $tip .detach() .css({ top: 0, left: 0, display: 'block' }) .addClass(placement) .data('bs.' + this.type, this) this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) this.$element.trigger('inserted.bs.' + this.type) var pos = this.getPosition() var actualWidth = $tip[0].offsetWidth var actualHeight = $tip[0].offsetHeight if (autoPlace) { var orgPlacement = placement var viewportDim = this.getPosition(this.$viewport) placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : placement $tip .removeClass(orgPlacement) .addClass(placement) } var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) this.applyPlacement(calculatedOffset, placement) var complete = function () { var prevHoverState = that.hoverState that.$element.trigger('shown.bs.' + that.type) that.hoverState = null if (prevHoverState == 'out') that.leave(that) } $.support.transition && this.$tip.hasClass('fade') ? $tip .one('bsTransitionEnd', complete) .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : complete() } } Tooltip.prototype.applyPlacement = function (offset, placement) { var $tip = this.tip() var width = $tip[0].offsetWidth var height = $tip[0].offsetHeight // manually read margins because getBoundingClientRect includes difference var marginTop = parseInt($tip.css('margin-top'), 10) var marginLeft = parseInt($tip.css('margin-left'), 10) // we must check for NaN for ie 8/9 if (isNaN(marginTop)) marginTop = 0 if (isNaN(marginLeft)) marginLeft = 0 offset.top += marginTop offset.left += marginLeft // $.fn.offset doesn't round pixel values // so we use setOffset directly with our own function B-0 $.offset.setOffset($tip[0], $.extend({ using: function (props) { $tip.css({ top: Math.round(props.top), left: Math.round(props.left) }) } }, offset), 0) $tip.addClass('in') // check to see if placing tip in new offset caused the tip to resize itself var actualWidth = $tip[0].offsetWidth var actualHeight = $tip[0].offsetHeight if (placement == 'top' && actualHeight != height) { offset.top = offset.top + height - actualHeight } var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) if (delta.left) offset.left += delta.left else offset.top += delta.top var isVertical = /top|bottom/.test(placement) var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' $tip.offset(offset) this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) } Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { this.arrow() .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') .css(isVertical ? 'top' : 'left', '') } Tooltip.prototype.setContent = function () { var $tip = this.tip() var title = this.getTitle() $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) $tip.removeClass('fade in top bottom left right') } Tooltip.prototype.hide = function (callback) { var that = this var $tip = $(this.$tip) var e = $.Event('hide.bs.' + this.type) function complete() { if (that.hoverState != 'in') $tip.detach() that.$element .removeAttr('aria-describedby') .trigger('hidden.bs.' + that.type) callback && callback() } this.$element.trigger(e) if (e.isDefaultPrevented()) return $tip.removeClass('in') $.support.transition && $tip.hasClass('fade') ? $tip .one('bsTransitionEnd', complete) .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : complete() this.hoverState = null return this } Tooltip.prototype.fixTitle = function () { var $e = this.$element if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') } } Tooltip.prototype.hasContent = function () { return this.getTitle() } Tooltip.prototype.getPosition = function ($element) { $element = $element || this.$element var el = $element[0] var isBody = el.tagName == 'BODY' var elRect = el.getBoundingClientRect() if (elRect.width == null) { // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) } var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null return $.extend({}, elRect, scroll, outerDims, elOffset) } Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } } Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { var delta = { top: 0, left: 0 } if (!this.$viewport) return delta var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 var viewportDimensions = this.getPosition(this.$viewport) if (/right|left/.test(placement)) { var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight if (topEdgeOffset < viewportDimensions.top) { // top overflow delta.top = viewportDimensions.top - topEdgeOffset } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset } } else { var leftEdgeOffset = pos.left - viewportPadding var rightEdgeOffset = pos.left + viewportPadding + actualWidth if (leftEdgeOffset < viewportDimensions.left) { // left overflow delta.left = viewportDimensions.left - leftEdgeOffset } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset } } return delta } Tooltip.prototype.getTitle = function () { var title var $e = this.$element var o = this.options title = $e.attr('data-original-title') || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) return title } Tooltip.prototype.getUID = function (prefix) { do prefix += ~~(Math.random() * 1000000) while (document.getElementById(prefix)) return prefix } Tooltip.prototype.tip = function () { if (!this.$tip) { this.$tip = $(this.options.template) if (this.$tip.length != 1) { throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') } } return this.$tip } Tooltip.prototype.arrow = function () { return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) } Tooltip.prototype.enable = function () { this.enabled = true } Tooltip.prototype.disable = function () { this.enabled = false } Tooltip.prototype.toggleEnabled = function () { this.enabled = !this.enabled } Tooltip.prototype.toggle = function (e) { var self = this if (e) { self = $(e.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(e.currentTarget, this.getDelegateOptions()) $(e.currentTarget).data('bs.' + this.type, self) } } if (e) { self.inState.click = !self.inState.click if (self.isInStateTrue()) self.enter(self) else self.leave(self) } else { self.tip().hasClass('in') ? self.leave(self) : self.enter(self) } } Tooltip.prototype.destroy = function () { var that = this clearTimeout(this.timeout) this.hide(function () { that.$element.off('.' + that.type).removeData('bs.' + that.type) if (that.$tip) { that.$tip.detach() } that.$tip = null that.$arrow = null that.$viewport = null }) } // TOOLTIP PLUGIN DEFINITION // ========================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.tooltip') var options = typeof option == 'object' && option if (!data && /destroy|hide/.test(option)) return if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.tooltip $.fn.tooltip = Plugin $.fn.tooltip.Constructor = Tooltip // TOOLTIP NO CONFLICT // =================== $.fn.tooltip.noConflict = function () { $.fn.tooltip = old return this } }(jQuery); /* ======================================================================== * Bootstrap: popover.js v3.3.5 * http://getbootstrap.com/javascript/#popovers * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // POPOVER PUBLIC CLASS DEFINITION // =============================== var Popover = function (element, options) { this.init('popover', element, options) } if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') Popover.VERSION = '3.3.5' Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { placement: 'right', trigger: 'click', content: '', template: '' }) // NOTE: POPOVER EXTENDS tooltip.js // ================================ Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) Popover.prototype.constructor = Popover Popover.prototype.getDefaults = function () { return Popover.DEFAULTS } Popover.prototype.setContent = function () { var $tip = this.tip() var title = this.getTitle() var content = this.getContent() $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' ](content) $tip.removeClass('fade top bottom left right in') // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do // this manually by checking the contents. if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() } Popover.prototype.hasContent = function () { return this.getTitle() || this.getContent() } Popover.prototype.getContent = function () { var $e = this.$element var o = this.options return $e.attr('data-content') || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content) } Popover.prototype.arrow = function () { return (this.$arrow = this.$arrow || this.tip().find('.arrow')) } // POPOVER PLUGIN DEFINITION // ========================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.popover') var options = typeof option == 'object' && option if (!data && /destroy|hide/.test(option)) return if (!data) $this.data('bs.popover', (data = new Popover(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.popover $.fn.popover = Plugin $.fn.popover.Constructor = Popover // POPOVER NO CONFLICT // =================== $.fn.popover.noConflict = function () { $.fn.popover = old return this } }(jQuery); /* ======================================================================== * Bootstrap: scrollspy.js v3.3.5 * http://getbootstrap.com/javascript/#scrollspy * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // SCROLLSPY CLASS DEFINITION // ========================== function ScrollSpy(element, options) { this.$body = $(document.body) this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) this.options = $.extend({}, ScrollSpy.DEFAULTS, options) this.selector = (this.options.target || '') + ' .nav li > a' this.offsets = [] this.targets = [] this.activeTarget = null this.scrollHeight = 0 this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) this.refresh() this.process() } ScrollSpy.VERSION = '3.3.5' ScrollSpy.DEFAULTS = { offset: 10 } ScrollSpy.prototype.getScrollHeight = function () { return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) } ScrollSpy.prototype.refresh = function () { var that = this var offsetMethod = 'offset' var offsetBase = 0 this.offsets = [] this.targets = [] this.scrollHeight = this.getScrollHeight() if (!$.isWindow(this.$scrollElement[0])) { offsetMethod = 'position' offsetBase = this.$scrollElement.scrollTop() } this.$body .find(this.selector) .map(function () { var $el = $(this) var href = $el.data('target') || $el.attr('href') var $href = /^#./.test(href) && $(href) return ($href && $href.length && $href.is(':visible') && [[$href[offsetMethod]().top + offsetBase, href]]) || null }) .sort(function (a, b) { return a[0] - b[0] }) .each(function () { that.offsets.push(this[0]) that.targets.push(this[1]) }) } ScrollSpy.prototype.process = function () { var scrollTop = this.$scrollElement.scrollTop() + this.options.offset var scrollHeight = this.getScrollHeight() var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() var offsets = this.offsets var targets = this.targets var activeTarget = this.activeTarget var i if (this.scrollHeight != scrollHeight) { this.refresh() } if (scrollTop >= maxScroll) { return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) } if (activeTarget && scrollTop < offsets[0]) { this.activeTarget = null return this.clear() } for (i = offsets.length; i--;) { activeTarget != targets[i] && scrollTop >= offsets[i] && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) && this.activate(targets[i]) } } ScrollSpy.prototype.activate = function (target) { this.activeTarget = target this.clear() var selector = this.selector + '[data-target="' + target + '"],' + this.selector + '[href="' + target + '"]' var active = $(selector) .parents('li') .addClass('active') if (active.parent('.dropdown-menu').length) { active = active .closest('li.dropdown') .addClass('active') } active.trigger('activate.bs.scrollspy') } ScrollSpy.prototype.clear = function () { $(this.selector) .parentsUntil(this.options.target, '.active') .removeClass('active') } // SCROLLSPY PLUGIN DEFINITION // =========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.scrollspy') var options = typeof option == 'object' && option if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.scrollspy $.fn.scrollspy = Plugin $.fn.scrollspy.Constructor = ScrollSpy // SCROLLSPY NO CONFLICT // ===================== $.fn.scrollspy.noConflict = function () { $.fn.scrollspy = old return this } // SCROLLSPY DATA-API // ================== $(window).on('load.bs.scrollspy.data-api', function () { $('[data-spy="scroll"]').each(function () { var $spy = $(this) Plugin.call($spy, $spy.data()) }) }) }(jQuery); /* ======================================================================== * Bootstrap: tab.js v3.3.5 * http://getbootstrap.com/javascript/#tabs * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // TAB CLASS DEFINITION // ==================== var Tab = function (element) { // jscs:disable requireDollarBeforejQueryAssignment this.element = $(element) // jscs:enable requireDollarBeforejQueryAssignment } Tab.VERSION = '3.3.5' Tab.TRANSITION_DURATION = 150 Tab.prototype.show = function () { var $this = this.element var $ul = $this.closest('ul:not(.dropdown-menu)') var selector = $this.data('target') if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } if ($this.parent('li').hasClass('active')) return var $previous = $ul.find('.active:last a') var hideEvent = $.Event('hide.bs.tab', { relatedTarget: $this[0] }) var showEvent = $.Event('show.bs.tab', { relatedTarget: $previous[0] }) $previous.trigger(hideEvent) $this.trigger(showEvent) if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return var $target = $(selector) this.activate($this.closest('li'), $ul) this.activate($target, $target.parent(), function () { $previous.trigger({ type: 'hidden.bs.tab', relatedTarget: $this[0] }) $this.trigger({ type: 'shown.bs.tab', relatedTarget: $previous[0] }) }) } Tab.prototype.activate = function (element, container, callback) { var $active = container.find('> .active') var transition = callback && $.support.transition && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) function next() { $active .removeClass('active') .find('> .dropdown-menu > .active') .removeClass('active') .end() .find('[data-toggle="tab"]') .attr('aria-expanded', false) element .addClass('active') .find('[data-toggle="tab"]') .attr('aria-expanded', true) if (transition) { element[0].offsetWidth // reflow for transition element.addClass('in') } else { element.removeClass('fade') } if (element.parent('.dropdown-menu').length) { element .closest('li.dropdown') .addClass('active') .end() .find('[data-toggle="tab"]') .attr('aria-expanded', true) } callback && callback() } $active.length && transition ? $active .one('bsTransitionEnd', next) .emulateTransitionEnd(Tab.TRANSITION_DURATION) : next() $active.removeClass('in') } // TAB PLUGIN DEFINITION // ===================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.tab') if (!data) $this.data('bs.tab', (data = new Tab(this))) if (typeof option == 'string') data[option]() }) } var old = $.fn.tab $.fn.tab = Plugin $.fn.tab.Constructor = Tab // TAB NO CONFLICT // =============== $.fn.tab.noConflict = function () { $.fn.tab = old return this } // TAB DATA-API // ============ var clickHandler = function (e) { e.preventDefault() Plugin.call($(this), 'show') } $(document) .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) }(jQuery); /* ======================================================================== * Bootstrap: affix.js v3.3.5 * http://getbootstrap.com/javascript/#affix * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // AFFIX CLASS DEFINITION // ====================== var Affix = function (element, options) { this.options = $.extend({}, Affix.DEFAULTS, options) this.$target = $(this.options.target) .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) this.$element = $(element) this.affixed = null this.unpin = null this.pinnedOffset = null this.checkPosition() } Affix.VERSION = '3.3.5' Affix.RESET = 'affix affix-top affix-bottom' Affix.DEFAULTS = { offset: 0, target: window } Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { var scrollTop = this.$target.scrollTop() var position = this.$element.offset() var targetHeight = this.$target.height() if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false if (this.affixed == 'bottom') { if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' } var initializing = this.affixed == null var colliderTop = initializing ? scrollTop : position.top var colliderHeight = initializing ? targetHeight : height if (offsetTop != null && scrollTop <= offsetTop) return 'top' if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' return false } Affix.prototype.getPinnedOffset = function () { if (this.pinnedOffset) return this.pinnedOffset this.$element.removeClass(Affix.RESET).addClass('affix') var scrollTop = this.$target.scrollTop() var position = this.$element.offset() return (this.pinnedOffset = position.top - scrollTop) } Affix.prototype.checkPositionWithEventLoop = function () { setTimeout($.proxy(this.checkPosition, this), 1) } Affix.prototype.checkPosition = function () { if (!this.$element.is(':visible')) return var height = this.$element.height() var offset = this.options.offset var offsetTop = offset.top var offsetBottom = offset.bottom var scrollHeight = Math.max($(document).height(), $(document.body).height()) if (typeof offset != 'object') offsetBottom = offsetTop = offset if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) if (this.affixed != affix) { if (this.unpin != null) this.$element.css('top', '') var affixType = 'affix' + (affix ? '-' + affix : '') var e = $.Event(affixType + '.bs.affix') this.$element.trigger(e) if (e.isDefaultPrevented()) return this.affixed = affix this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null this.$element .removeClass(Affix.RESET) .addClass(affixType) .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') } if (affix == 'bottom') { this.$element.offset({ top: scrollHeight - height - offsetBottom }) } } // AFFIX PLUGIN DEFINITION // ======================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.affix') var options = typeof option == 'object' && option if (!data) $this.data('bs.affix', (data = new Affix(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.affix $.fn.affix = Plugin $.fn.affix.Constructor = Affix // AFFIX NO CONFLICT // ================= $.fn.affix.noConflict = function () { $.fn.affix = old return this } // AFFIX DATA-API // ============== $(window).on('load', function () { $('[data-spy="affix"]').each(function () { var $spy = $(this) var data = $spy.data() data.offset = data.offset || {} if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom if (data.offsetTop != null) data.offset.top = data.offsetTop Plugin.call($spy, data) }) }) }(jQuery); ================================================ FILE: static/js/floatButton.js ================================================ //JS For FloatButton to goTop, goBottom and refresh function getCookie(name) { var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); return r ? r[1] : undefined; } function getAlertHtml(category, message) { var s = '
'+ ''+ message+'
'; return s; } function alertXtg(category, message, timeout) { s = getAlertHtml(category, message); $('body').append(s); if(timeout) { setTimeout(function () { $('#fix-alert').remove(); }, timeout); } } function codeHighLight() { if(typeof(hljs) != "undefined" ) { $('pre code').each(function (i, block) { hljs.highlightBlock(block); }); } } $(document).ready(function(){ //$('#goTop').click(function(){ // $(window).scrollTop(0); //}); $('#goTop').click(function(){ $('html, body').animate({scrollTop: '0px'}, 800); }); $('#refresh').click(function(){ window.location.reload(); }); $('#goBottom').click(function(){ $('html, body').animate({scrollTop: $('.footer').offset().top}, 800); }); }); ================================================ FILE: static/js/markdown/bootstrap-markdown.js ================================================ /* =================================================== * bootstrap-markdown.js v2.10.0 * http://github.com/toopay/bootstrap-markdown * =================================================== * Copyright 2013-2016 Taufan Aditya * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================== */ (function(factory) { if (typeof define === "function" && define.amd) { //RequireJS define(["jquery"], factory); } else if (typeof exports === 'object') { //Backbone.js factory(require('jquery')); } else { //Jquery plugin factory(jQuery); } }(function($) { "use strict"; /* MARKDOWN CLASS DEFINITION * ========================== */ var Markdown = function(element, options) { // @TODO : remove this BC on next major release // @see : https://github.com/toopay/bootstrap-markdown/issues/109 var opts = ['autofocus', 'savable', 'hideable', 'width', 'height', 'resize', 'iconlibrary', 'language', 'footer', 'fullscreen', 'hiddenButtons', 'disabledButtons' ]; $.each(opts, function(_, opt) { if (typeof $(element).data(opt) !== 'undefined') { options = typeof options == 'object' ? options : {}; options[opt] = $(element).data(opt); } }); // End BC // Class Properties this.$ns = 'bootstrap-markdown'; this.$element = $(element); this.$editable = { el: null, type: null, attrKeys: [], attrValues: [], content: null }; this.$options = $.extend(true, {}, $.fn.markdown.defaults, options, this.$element.data('options')); this.$oldContent = null; this.$isPreview = false; this.$isFullscreen = false; this.$editor = null; this.$textarea = null; this.$handler = []; this.$callback = []; this.$nextTab = []; this.showEditor(); }; Markdown.prototype = { constructor: Markdown, __alterButtons: function(name, alter) { var handler = this.$handler, isAll = (name == 'all'), that = this; $.each(handler, function(k, v) { var halt = true; if (isAll) { halt = false; } else { halt = v.indexOf(name) < 0; } if (halt === false) { alter(that.$editor.find('button[data-handler="' + v + '"]')); } }); }, __buildButtons: function(buttonsArray, container) { var i, ns = this.$ns, handler = this.$handler, callback = this.$callback; for (i = 0; i < buttonsArray.length; i++) { // Build each group container var y, btnGroups = buttonsArray[i]; for (y = 0; y < btnGroups.length; y++) { // Build each button group var z, buttons = btnGroups[y].data, btnGroupContainer = $('
', { 'class': 'btn-group' }); for (z = 0; z < buttons.length; z++) { var button = buttons[z], buttonContainer, buttonIconContainer, buttonHandler = ns + '-' + button.name, buttonIcon = this.__getIcon(button), btnText = button.btnText ? button.btnText : '', btnClass = button.btnClass ? button.btnClass : 'btn', tabIndex = button.tabIndex ? button.tabIndex : '-1', hotkey = typeof button.hotkey !== 'undefined' ? button.hotkey : '', hotkeyCaption = typeof jQuery.hotkeys !== 'undefined' && hotkey !== '' ? ' (' + hotkey + ')' : ''; // Construct the button object buttonContainer = $(''); buttonContainer.text(' ' + this.__localize(btnText)).addClass('btn-default btn-sm').addClass(btnClass); if (btnClass.match(/btn\-(primary|success|info|warning|danger|link)/)) { buttonContainer.removeClass('btn-default'); } buttonContainer.attr({ 'type': 'button', 'title': this.__localize(button.title) + hotkeyCaption, 'tabindex': tabIndex, 'data-provider': ns, 'data-handler': buttonHandler, 'data-hotkey': hotkey }); if (button.toggle === true) { buttonContainer.attr('data-toggle', 'button'); } buttonIconContainer = $(''); buttonIconContainer.addClass(buttonIcon); buttonIconContainer.prependTo(buttonContainer); // Attach the button object btnGroupContainer.append(buttonContainer); // Register handler and callback handler.push(buttonHandler); callback.push(button.callback); } // Attach the button group into container dom container.append(btnGroupContainer); } } return container; }, __setListener: function() { // Set size and resizable Properties var hasRows = typeof this.$textarea.attr('rows') !== 'undefined', maxRows = this.$textarea.val().split("\n").length > 5 ? this.$textarea.val().split("\n").length : '5', rowsVal = hasRows ? this.$textarea.attr('rows') : maxRows; this.$textarea.attr('rows', rowsVal); if (this.$options.resize) { this.$textarea.css('resize', this.$options.resize); } this.$textarea.on({ 'focus': $.proxy(this.focus, this), 'keyup': $.proxy(this.keyup, this), 'change': $.proxy(this.change, this), 'select': $.proxy(this.select, this) }); if (this.eventSupported('keydown')) { this.$textarea.on('keydown', $.proxy(this.keydown, this)); } if (this.eventSupported('keypress')) { this.$textarea.on('keypress', $.proxy(this.keypress, this)); } // Re-attach markdown data this.$textarea.data('markdown', this); }, __handle: function(e) { var target = $(e.currentTarget), handler = this.$handler, callback = this.$callback, handlerName = target.attr('data-handler'), callbackIndex = handler.indexOf(handlerName), callbackHandler = callback[callbackIndex]; // Trigger the focusin $(e.currentTarget).focus(); callbackHandler(this); // Trigger onChange for each button handle this.change(this); // Unless it was the save handler, // focusin the textarea if (handlerName.indexOf('cmdSave') < 0) { this.$textarea.focus(); } e.preventDefault(); }, __localize: function(string) { var messages = $.fn.markdown.messages, language = this.$options.language; if ( typeof messages !== 'undefined' && typeof messages[language] !== 'undefined' && typeof messages[language][string] !== 'undefined' ) { return messages[language][string]; } return string; }, __getIcon: function(src) { if(typeof src == 'object'){ var customIcon = this.$options.customIcons[src.name]; return typeof customIcon == 'undefined' ? src.icon[this.$options.iconlibrary] : customIcon; } else { return src; } }, setFullscreen: function(mode) { var $editor = this.$editor, $textarea = this.$textarea; if (mode === true) { $editor.addClass('md-fullscreen-mode'); $('body').addClass('md-nooverflow'); this.$options.onFullscreen(this); } else { $editor.removeClass('md-fullscreen-mode'); $('body').removeClass('md-nooverflow'); this.$options.onFullscreenExit(this); if (this.$isPreview === true) this.hidePreview().showPreview(); } this.$isFullscreen = mode; $textarea.focus(); }, showEditor: function() { var instance = this, textarea, ns = this.$ns, container = this.$element, originalHeigth = container.css('height'), originalWidth = container.css('width'), editable = this.$editable, handler = this.$handler, callback = this.$callback, options = this.$options, editor = $('
', { 'class': 'md-editor', click: function() { instance.focus(); } }); // Prepare the editor if (this.$editor === null) { // Create the panel var editorHeader = $('
', { 'class': 'md-header btn-toolbar' }); // Merge the main & additional button groups together var allBtnGroups = []; if (options.buttons.length > 0) allBtnGroups = allBtnGroups.concat(options.buttons[0]); if (options.additionalButtons.length > 0) { // iterate the additional button groups $.each(options.additionalButtons[0], function(idx, buttonGroup) { // see if the group name of the addional group matches an existing group var matchingGroups = $.grep(allBtnGroups, function(allButtonGroup, allIdx) { return allButtonGroup.name === buttonGroup.name; }); // if it matches add the addional buttons to that group, if not just add it to the all buttons group if (matchingGroups.length > 0) { matchingGroups[0].data = matchingGroups[0].data.concat(buttonGroup.data); } else { allBtnGroups.push(options.additionalButtons[0][idx]); } }); } // Reduce and/or reorder the button groups if (options.reorderButtonGroups.length > 0) { allBtnGroups = allBtnGroups .filter(function(btnGroup) { return options.reorderButtonGroups.indexOf(btnGroup.name) > -1; }) .sort(function(a, b) { if (options.reorderButtonGroups.indexOf(a.name) < options.reorderButtonGroups.indexOf(b.name)) return -1; if (options.reorderButtonGroups.indexOf(a.name) > options.reorderButtonGroups.indexOf(b.name)) return 1; return 0; }); } // Build the buttons if (allBtnGroups.length > 0) { editorHeader = this.__buildButtons([allBtnGroups], editorHeader); } if (options.fullscreen.enable) { editorHeader.append('
').on('click', '.md-control-fullscreen', function(e) { e.preventDefault(); instance.setFullscreen(true); }); } editor.append(editorHeader); // Wrap the textarea if (container.is('textarea')) { container.before(editor); textarea = container; textarea.addClass('md-input'); editor.append(textarea); } else { var rawContent = (typeof toMarkdown == 'function') ? toMarkdown(container.html()) : container.html(), currentContent = $.trim(rawContent); // This is some arbitrary content that could be edited textarea = $('
{% end %} {% block private_script %} {% end %} ================================================ FILE: template/admin/blog_plugin_edit.html ================================================ {% extends 'admin_base.html' %} {% block title2 %} 修改插件 {% end %} {% block admin_content %}

添加插件


{% module xsrf_form_html() %}
(可选)
{% end %} {% block private_script %} {% end %} ================================================ FILE: template/admin/custom_blog_info.html ================================================ {% from model.site_info import SiteCollection %} {% extends 'admin_base.html' %} {% block title2 %} 基本信息 {% end %} {% block admin_content %}

基本信息


博客标题:
{{ SiteCollection.title }}
个性签名:
{{ SiteCollection.signature }}
导航样式:
{% if SiteCollection.navbar in navbar_styles %} {{ navbar_styles[SiteCollection.navbar] }} {% else %} {{ navbar_styles['default'] }} {% end %}
{% end %} ================================================ FILE: template/admin/custom_blog_plugin.html ================================================ {% extends 'admin_base.html' %} {% block title2 %} 插件管理 {% end %} {% block admin_content %}

插件管理


插件总数:{{ pager.totalCount }}

{% for plugin in pager.result %} {% if plugin.content != 'system_plugin' %} {% else %} {% end %} {% end %}
序号 插件名称 备注 排序 启用 修改 删除
{{ plugin.order }} {{ plugin.title }} {{ plugin.note }} {% if plugin.disabled %} {% else %} {% end %}
{% module Template("_macros.html", pager=pager, url=reverse_url('admin.custom.blog_plugin'), params=None) %}
{% end %} ================================================ FILE: template/admin/help_page.html ================================================ {% extends 'admin_base.html' %} {% block title2 %} 帮助 {% end %} {% block admin_content %}

blog_xtg帮助页面


一、blog_xtg简介

blog_xtg 是作者xiaotaogou基于Blog_mini重构的个人分布式博客系统。

由于不太擅长前端,所以基本照搬Blog_mini的页面,但是整个后端逻辑都是重写的,以下是与Blog_mini的主要区别:

  1. 改用tornado框架,是个基于异步IO的web server。
  2. 分布式架构,可以多进程多主机启动server实例,再通过nginx等代理服务器做负载均衡,实现横向扩展提高并发性能。
  3. 提高多数主要页面访问性能。对频繁查询的组件(例如博客标题、菜单、公告、访问统计)进行缓存,优化sql查询(多条sql语句合并一次执行、仅查需要的字段,例如搜索博文列表不查博文的具体内容)以提高首页博文等主要页面访问性能。
  4. 访问统计改为日pv和日uv。
  5. 博文编辑器改为markdown编辑器。
  6. 引入alembic管理数据库版本。
  7. 可使用docker快速部署。
二、开源声明
  1. blog_xtg是完全开源的,你可以在GitHub获取他的全部代码,当然也可以对他进行二次开发。
  2. 作者不对任何基于blog_xtg开发的或搭建的项目、站点负责。
  3. 如果没有获得作者授权,请勿将其商用。
三、技术支持

如果你有任何疑问,可以给作者留言:

附:

{% end %} ================================================ FILE: template/admin/manage_articleTypes.html ================================================ {% extends 'admin_base.html' %} {% block title2 %} 博文分类 {% end %} {% block admin_content %}

博文分类


分类总数:{{ pager.totalCount }}

{% for articleType in pager.result %} {% if not articleType.is_protected %} {% else %} {% end %}
分类名称 分类介绍 属性 所属导航 博文数量 修改 删除
{{ articleType.name }} {% if articleType.introduction %} {{ articleType.introduction }} {% else %} 该分类暂时没有介绍 {% end %} {% if articleType.is_hide %} 隐藏 {% else %} 公开 {% end %} {% if articleType.menu %} {{ articleType.menu.name }} {% else %} 该分类暂时没有所属导航 {% end %} {{ articleType.articles_count }} {% end %}
{% module Template("_macros.html", pager=pager, url=reverse_url('admin.articleTypes'), params=None) %}
{% end %} ================================================ FILE: template/admin/manage_articleTypes_nav.html ================================================ {% extends 'admin_base.html' %} {% block title2 %} 分类导航 {% end %} {% block admin_content %}

分类导航


导航总数:{{ pager.totalCount }}

{% for menu in pager.result %} {% end %}
序号 导航名称 所含分类 排序 修改 删除
{{ menu.order }} {{ menu.name }} {% if menu.all_types %} {% for articleType in menu.all_types %} {{ articleType.name }} {% end %} {% else %} 没有分类,该导航将不显示。 {% end %}
{% module Template("_macros.html", pager=pager, url=reverse_url('admin.articleTypeNavs'), params=None) %}
{% end %} ================================================ FILE: template/admin/manage_articles.html ================================================ {% extends 'admin_base.html' %} {% block title2 %} 管理博文 {% end %} {% block admin_content %}

管理博文


博文总数:{{ pager.totalCount }}

{% for article in pager.result %} {% end %}
全选 博文标题 来源 分类 发表日期 编辑 删除
{{ article.title }} {{ article.source.name }} {{ article.articleType.name }} {{ article.create_time.strftime("%Y-%m-%d %H:%M:%S") }}
{% module Template("_macros.html", pager=pager, url=reverse_url('admin.articles'), params=article_search_params.to_url_params()) %}
{% end %} ================================================ FILE: template/admin/manage_comments.html ================================================ {% extends 'admin_base.html' %} {% block title2 %} 博文评论 {% end %} {% block admin_content %}

博文评论


评论总数:{{ pager.totalCount }}

    {% for comment in pager.result %}
  •  
    {{ comment.author_name }} {{ comment.author_email }} {% if comment.rank == Constants.COMMENT_RANK_ADMIN %} 管理员 {% end %} 在《{{ comment.article.title }}》 的 {{comment.floor}}楼 发表评论: {% if comment.disabled %} {% else %} {% end %}
    {{ comment.create_time.strftime("%Y年%m月%d日 %H:%M:%S") }}
    {% if comment.comment_type == Constants.COMMENT_TYPE_REPLY %}

    回复给 {{ comment.reply_to_floor }}:

    {% end %}

    {{ comment.content }}

    {% if comment.disabled %}

    该评论已在博文页面中屏蔽!

    {% end %}
  • {% end %}
{% module Template("_macros.html", pager=pager, url=reverse_url('admin.comments'), params=None) %}
{% end %} ================================================ FILE: template/admin/submit_articles.html ================================================ {% extends 'admin_base.html' %} {% block title2 %} 发表博文 {% end %} {% block private_stylesheet %} {% end %} {% block admin_content %}

发表博文


{% module xsrf_form_html() %}
(1-50字)
(显示在博客首页,不填的话,默认取文章前120个字)
{% end %} {% block private_script %} {% end %} ================================================ FILE: template/article_detials.html ================================================ {% extends 'base.html' %} {% block title %} {{ article.title }} {% end %} {% block private_stylesheet %} {% end %} {% block content %}

{{ article.title }}




博文最后更新时间: {{ article.create_time.strftime("%Y年%m月%d日 %H:%M:%S") }}

{% if current_user %}
{% end %}

评论

{% include "_article_comments.html" %} {% module Template("_macros.html", pager=comments_pager, url=reverse_url('article', article.id), params="#comments") %}

发表评论

{% module xsrf_form_html() %}
{% end %} {% block script %} {% end %} ================================================ FILE: template/auth/login.html ================================================ 登陆blog_mhq管理后台 ================================================ FILE: template/base.html ================================================ {% from model.site_info import SiteCollection %} {% from model.constants import Constants %} {% block title %} {{ SiteCollection.title }} {% end %} {% block stylesheet %} {% block private_stylesheet %} {% end %} {% end %}

{{ SiteCollection.title }}

{{ SiteCollection.signature }}

{% block main %}
{% if handler.has_message() %} {% for message in handler.read_messages() %}
{{ message['message'] }}
{% end %} {% end %} {% block content %} {% end %}
{% block blog_nav_plugin %} {% for plugin in SiteCollection.plugins %} {% if not plugin.disabled %} {% if plugin.content != Constants.SYSTEM_PLUGIN %}
{{ plugin.title }}
{% raw plugin.content %}
{% else %}
博客统计

今日PV:{{ SiteCollection.pv }} 今日UV:{{ SiteCollection.uv }}

博文总数:{{ SiteCollection.article_count }} 评论总数:{{ SiteCollection.comment_count }}

{% for source in SiteCollection.article_sources %} {{ source.name }}{{ source.articles_count }} {% end %}

{% end %} {% end %} {% end %} {% end %}
{% end %}
{% block base_script %} {% end %} {% block script %} {% end %} ================================================ FILE: template/index.html ================================================ {% extends 'base.html' %} {% block content %} {% if pager and pager.result %} {% for article in pager.result %}

{{ article.title }}

{{ article.summary }}{% if len(article.summary) >= 100 %}...... {%end%}

{% end %} {% end %} {% module Template("_macros.html", pager=pager, url=base_url, params=article_search_params.to_url_params()) %} {% end %} ================================================ FILE: template/super/init.html ================================================ 创建管理员账户 ================================================ FILE: url_mapping.py ================================================ # coding=utf-8 import controller.home import controller.admin import controller.admin_custom import controller.admin_article_type import controller.admin_article import controller.super from tornado.web import url # url映射 handlers = [ url(r"/", controller.home.HomeHandler, name="index"), url(r"/auth/login", controller.home.LoginHandler, name="login"), url(r"/auth/logout", controller.home.LogoutHandler, name="logout"), # articleSource url(r"/source/([0-9]+)/articles", controller.home.articleSourceHandler, name="articleSource"), # articleType url(r"/type/([0-9]+)/articles", controller.home.ArticleTypeHandler, name="articleType"), # article url(r"/article/([0-9]+)", controller.home.ArticleHandler, name="article"), url(r"/article/([0-9]+)/comment", controller.home.ArticleCommentHandler, name="articleComment"), # admin url(r"/admin/account", controller.admin.AdminAccountHandler, name="admin.account"), url(r"/admin/help", controller.admin.AdminHelpHandler, name="admin.help"), url(r"/admin/account/(change-password|edit-user-info)", controller.admin.AdminAccountHandler, name="admin.account.update"), # admin.custom url(r"/admin/custom/blog-info", controller.admin_custom.AdminCustomBlogInfoHandler, name="admin.custom.blog_info"), url(r"/admin/custom/blog-plugin", controller.admin_custom.AdminCustomBlogPluginHandler, name="admin.custom.blog_plugin"), url(r"/admin/custom/blog-plugin/(add)", controller.admin_custom.AdminCustomBlogPluginHandler, name="admin.custom.plugin.action"), url(r"/admin/custom/blog-plugin/([0-9]+)/(sort-down|sort-up|disable|enable|edit|delete)", controller.admin_custom.AdminCustomBlogPluginHandler, name="admin.custom.plugin.update"), # admin.article_type url(r"/admin/articleType", controller.admin_article_type.AdminArticleTypeHandler, name="admin.articleTypes"), url(r"/admin/articleType/(add)", controller.admin_article_type.AdminArticleTypeHandler, name="admin.articleType.action"), url(r"/admin/articleType/([0-9]+)/(delete|update)", controller.admin_article_type.AdminArticleTypeHandler, name="admin.articleType.update"), # admin.article_type_nav (menu) url(r"/admin/articleType/nav", controller.admin_article_type.AdminArticleTypeNavHandler, name="admin.articleTypeNavs"), url(r"/admin/articleType/nav/(add)", controller.admin_article_type.AdminArticleTypeNavHandler, name="admin.articleTypeNav.action"), url(r"/admin/articleType/nav/([0-9]+)/(sort-down|sort-up|delete|update)", controller.admin_article_type.AdminArticleTypeNavHandler, name="admin.articleTypeNav.update"), # admin.article url(r"/admin/article/(submit)", controller.admin_article.AdminArticleHandler, name="admin.article.action"), url(r"/admin/article", controller.admin_article.AdminArticleHandler, name="admin.articles"), url(r"/admin/article/([0-9]+)", controller.admin_article.AdminArticleHandler, name="admin.article"), url(r"/admin/article/([0-9]+)/(delete)", controller.admin_article.AdminArticleHandler, name="admin.article.update"), url(r"/admin/comment", controller.admin_article.AdminArticleCommentHandler, name="admin.comments"), url(r"/admin/article/([0-9]+)/comment/([0-9]+)/(disable|enable|delete)", controller.admin_article.AdminArticleCommentHandler, name="admin.comment.update"), # super.init url(r"/super/init", controller.super.SuperHandler, name="super.init"), ]