├── logs └── .gitignore ├── .gitignore ├── extends ├── __init__.py ├── time_task.py ├── utils.py ├── session_redis.py ├── cache_tornadis.py ├── session_tornadis.py └── pub_sub_tornadis.py ├── model ├── __init__.py ├── search_params │ ├── __init__.py │ ├── menu_params.py │ ├── plugin_params.py │ ├── article_type_params.py │ ├── comment_params.py │ └── article_params.py ├── constants.py ├── site_info.py ├── logined_user.py └── pager.py ├── controller ├── __init__.py ├── super.py └── admin.py ├── alembic ├── README ├── script.py.mako └── env.py ├── static ├── favicon.ico ├── images │ ├── background.jpg │ └── loading154.gif ├── tinymce │ └── js │ │ └── tinymce │ │ ├── skins │ │ ├── myskin │ │ │ ├── fonts │ │ │ │ ├── readme.md │ │ │ │ ├── tinymce.eot │ │ │ │ ├── tinymce.ttf │ │ │ │ ├── tinymce.woff │ │ │ │ ├── tinymce-small.eot │ │ │ │ ├── tinymce-small.ttf │ │ │ │ └── tinymce-small.woff │ │ │ ├── img │ │ │ │ ├── anchor.gif │ │ │ │ ├── loader.gif │ │ │ │ ├── object.gif │ │ │ │ └── trans.gif │ │ │ ├── content.min.css │ │ │ ├── content.inline.min.css │ │ │ └── skin.json │ │ └── lightgray │ │ │ ├── img │ │ │ ├── anchor.gif │ │ │ ├── loader.gif │ │ │ ├── object.gif │ │ │ └── trans.gif │ │ │ ├── fonts │ │ │ ├── tinymce.eot │ │ │ ├── tinymce.ttf │ │ │ ├── tinymce.woff │ │ │ ├── tinymce-small.eot │ │ │ ├── tinymce-small.ttf │ │ │ └── tinymce-small.woff │ │ │ ├── content.inline.min.css │ │ │ └── content.min.css │ │ ├── plugins │ │ ├── example_dependency │ │ │ └── plugin.min.js │ │ ├── media │ │ │ └── moxieplayer.swf │ │ ├── emoticons │ │ │ ├── img │ │ │ │ ├── smiley-cool.gif │ │ │ │ ├── smiley-cry.gif │ │ │ │ ├── smiley-kiss.gif │ │ │ │ ├── smiley-wink.gif │ │ │ │ ├── smiley-yell.gif │ │ │ │ ├── smiley-frown.gif │ │ │ │ ├── smiley-sealed.gif │ │ │ │ ├── smiley-smile.gif │ │ │ │ ├── smiley-innocent.gif │ │ │ │ ├── smiley-laughing.gif │ │ │ │ ├── smiley-surprised.gif │ │ │ │ ├── smiley-undecided.gif │ │ │ │ ├── smiley-embarassed.gif │ │ │ │ ├── smiley-money-mouth.gif │ │ │ │ ├── smiley-tongue-out.gif │ │ │ │ └── smiley-foot-in-mouth.gif │ │ │ └── plugin.min.js │ │ ├── example │ │ │ ├── dialog.html │ │ │ └── plugin.min.js │ │ ├── print │ │ │ └── plugin.min.js │ │ ├── hr │ │ │ └── plugin.min.js │ │ ├── anchor │ │ │ └── plugin.min.js │ │ ├── nonbreaking │ │ │ └── plugin.min.js │ │ ├── code │ │ │ └── plugin.min.js │ │ ├── directionality │ │ │ └── plugin.min.js │ │ ├── contextmenu │ │ │ └── plugin.min.js │ │ ├── wordcount │ │ │ └── plugin.min.js │ │ ├── noneditable │ │ │ └── plugin.min.js │ │ ├── save │ │ │ └── plugin.min.js │ │ ├── visualblocks │ │ │ ├── plugin.min.js │ │ │ └── css │ │ │ │ └── visualblocks.css │ │ ├── colorpicker │ │ │ └── plugin.min.js │ │ ├── pagebreak │ │ │ └── plugin.min.js │ │ ├── visualchars │ │ │ └── plugin.min.js │ │ ├── tabfocus │ │ │ └── plugin.min.js │ │ ├── advlist │ │ │ └── plugin.min.js │ │ ├── preview │ │ │ └── plugin.min.js │ │ ├── fullscreen │ │ │ └── plugin.min.js │ │ ├── autoresize │ │ │ └── plugin.min.js │ │ ├── autolink │ │ │ └── plugin.min.js │ │ ├── insertdatetime │ │ │ └── plugin.min.js │ │ ├── autosave │ │ │ └── plugin.min.js │ │ ├── importcss │ │ │ └── plugin.min.js │ │ ├── textpattern │ │ │ └── plugin.min.js │ │ ├── layer │ │ │ └── plugin.min.js │ │ ├── codesample │ │ │ └── css │ │ │ │ └── prism.css │ │ ├── bbcode │ │ │ └── plugin.min.js │ │ ├── legacyoutput │ │ │ └── plugin.min.js │ │ ├── textcolor │ │ │ └── plugin.min.js │ │ ├── template │ │ │ └── plugin.min.js │ │ └── link │ │ │ └── plugin.min.js │ │ ├── langs │ │ └── readme.md │ │ └── jquery.tinymce.min.js ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── js │ ├── super.js │ ├── npm.js │ ├── markdownEdit.js │ ├── markdown │ │ └── locale │ │ │ └── bootstrap-markdown.zh.js │ ├── floatButton.js │ ├── tinymce_setup.js │ └── articleDetail.js └── css │ ├── highlight │ └── default.min.css │ ├── bootstrap-markdown.min.css │ └── prism.css ├── docker ├── entrypoint.sh └── nginx.conf ├── requirements.txt ├── template ├── 403.html ├── 404.html ├── 500.html ├── admin │ ├── blog_plugin_add.html │ ├── blog_plugin_edit.html │ ├── help_page.html │ ├── admin_base.html │ ├── submit_articles.html │ ├── custom_blog_info.html │ ├── admin_account.html │ └── custom_blog_plugin.html ├── index.html ├── super │ └── init.html ├── _macros.html ├── auth │ └── login.html ├── article_detials.html └── _article_comments.html ├── service ├── __init__.py ├── custom_service.py ├── blog_view_service.py ├── pubsub_service.py ├── user_service.py ├── menu_service.py ├── comment_service.py ├── plugin_service.py └── article_type_service.py ├── log_config.py ├── alembic.ini ├── config.py ├── url_mapping.py └── README.md /logs/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | -------------------------------------------------------------------------------- /extends/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | -------------------------------------------------------------------------------- /model/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | -------------------------------------------------------------------------------- /controller/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /model/search_params/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/images/background.jpg -------------------------------------------------------------------------------- /static/images/loading154.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/images/loading154.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/fonts/readme.md: -------------------------------------------------------------------------------- 1 | Icons are generated and provided by the http://icomoon.io service. 2 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/example_dependency/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("example_dependency",function(){},["example"]); -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ "$1" == "upgradedb" ] 5 | then 6 | python main.py upgradedb 7 | fi 8 | exec supervisord -n 9 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/img/anchor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/myskin/img/anchor.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/img/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/myskin/img/loader.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/img/object.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/myskin/img/object.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/img/trans.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/myskin/img/trans.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/media/moxieplayer.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/media/moxieplayer.swf -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/lightgray/img/anchor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/lightgray/img/anchor.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/lightgray/img/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/lightgray/img/loader.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/lightgray/img/object.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/lightgray/img/object.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/lightgray/img/trans.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/lightgray/img/trans.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/fonts/tinymce.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/myskin/fonts/tinymce.eot -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/fonts/tinymce.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/myskin/fonts/tinymce.ttf -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tornado==4.4.2 2 | sqlalchemy==1.0.15 3 | tornadis==0.8.0 4 | futures==3.0.5 5 | alembic==0.9.1 6 | apscheduler==3.3.1 7 | mysql-connector-python==8.0.23 -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/fonts/tinymce.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/myskin/fonts/tinymce.woff -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce.eot -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce.ttf -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce.woff -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-cool.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-cool.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-cry.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-cry.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-kiss.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-kiss.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-wink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-wink.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-yell.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-yell.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/fonts/tinymce-small.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/myskin/fonts/tinymce-small.eot -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/fonts/tinymce-small.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/myskin/fonts/tinymce-small.ttf -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/fonts/tinymce-small.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/myskin/fonts/tinymce-small.woff -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-frown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-frown.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-sealed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-sealed.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-smile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-smile.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce-small.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce-small.eot -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce-small.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce-small.ttf -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce-small.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/skins/lightgray/fonts/tinymce-small.woff -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-innocent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-innocent.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-laughing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-laughing.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-surprised.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-surprised.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-undecided.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-undecided.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-embarassed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-embarassed.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-money-mouth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-money-mouth.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-tongue-out.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-tongue-out.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/img/smiley-foot-in-mouth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtg20121013/blog_xtg/HEAD/static/tinymce/js/tinymce/plugins/emoticons/img/smiley-foot-in-mouth.gif -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/langs/readme.md: -------------------------------------------------------------------------------- 1 | This is where language files should be placed. 2 | 3 | Please DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/ 4 | -------------------------------------------------------------------------------- /template/403.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | 禁止访问 5 | {% end %} 6 | {% block content %} 7 | 10 | {% end %} 11 | -------------------------------------------------------------------------------- /template/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | 无法找到该页面 5 | {% end %} 6 | {% block content %} 7 | 10 | {% end %} 11 | -------------------------------------------------------------------------------- /template/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | 服务器内部错误 5 | {% end %} 6 | {% block content %} 7 | 10 | {% end %} 11 | -------------------------------------------------------------------------------- /model/search_params/menu_params.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | class MenuSearchParams(object): 3 | 4 | ORDER_MODE_ORDER_ASC = 1 5 | 6 | def __init__(self, request): 7 | self.order_mode = request.get_argument("order_mode", MenuSearchParams.ORDER_MODE_ORDER_ASC) 8 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/example/dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Custom dialog

5 | Input some text: 6 | 7 | 8 | -------------------------------------------------------------------------------- /model/search_params/plugin_params.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | 4 | class PluginSearchParams(object): 5 | 6 | ORDER_MODE_ORDER_ASC = 1 7 | 8 | def __init__(self, request): 9 | self.order_mode = request.get_argument("order_mode", PluginSearchParams.ORDER_MODE_ORDER_ASC) 10 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/print/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("print",function(a){a.addCommand("mcePrint",function(){a.getWin().print()}),a.addButton("print",{title:"Print",cmd:"mcePrint"}),a.addShortcut("Meta+P","","mcePrint"),a.addMenuItem("print",{text:"Print",cmd:"mcePrint",icon:"print",shortcut:"Meta+P",context:"file"})}); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/hr/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("hr",function(a){a.addCommand("InsertHorizontalRule",function(){a.execCommand("mceInsertContent",!1,"
")}),a.addButton("hr",{icon:"hr",tooltip:"Horizontal line",cmd:"InsertHorizontalRule"}),a.addMenuItem("hr",{icon:"hr",text:"Horizontal line",cmd:"InsertHorizontalRule",context:"insert"})}); -------------------------------------------------------------------------------- /model/search_params/article_type_params.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | class ArticleTypeSearchParams(object): 3 | 4 | ORDER_MODE_ID_DESC = 1 5 | 6 | def __init__(self, request): 7 | self.order_mode = request.get_argument("order_mode", ArticleTypeSearchParams.ORDER_MODE_ID_DESC) 8 | self.show_setting = False 9 | self.show_articles_count = False 10 | -------------------------------------------------------------------------------- /static/js/super.js: -------------------------------------------------------------------------------- 1 | function checkPasswordForm() { 2 | var password = $('#password').val(); 3 | var password2 = $('#password2').val(); 4 | if (password != password2) { 5 | $('#group_password2').addClass('has-error'); 6 | $('#password2_err').show(); 7 | return false; 8 | } else { 9 | // $('#changePasswordForm').submit(); 10 | return true; 11 | } 12 | } -------------------------------------------------------------------------------- /service/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | 4 | class BaseService(object): 5 | @staticmethod 6 | def query_pager(query, pager, count=None): 7 | if count: 8 | pager.set_total_count(count) 9 | else: 10 | pager.set_total_count(query.count()) 11 | query_result = pager.build_query(query) 12 | pager.set_result(query_result.all()) 13 | return pager 14 | -------------------------------------------------------------------------------- /model/search_params/comment_params.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | class CommentSearchParams(object): 3 | 4 | ORDER_MODE_CREATE_TIME_ASC = 1 5 | ORDER_MODE_CREATE_TIME_DESC = 2 6 | 7 | def __init__(self, request): 8 | self.order_mode = request.get_argument("order_mode", CommentSearchParams.ORDER_MODE_CREATE_TIME_ASC) 9 | self.article_id = request.get_argument("article_id", None) 10 | self.show_article_id_title = False 11 | -------------------------------------------------------------------------------- /static/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /model/constants.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | 4 | class Constants(object): 5 | SYSTEM_PLUGIN = "system_plugin" 6 | 7 | COMMENT_RANK_ADMIN = "admin" 8 | COMMENT_RANK_NORMAL = "normal" 9 | COMMENT_TYPE_COMMENT = "comment" 10 | COMMENT_TYPE_REPLY = "reply" 11 | 12 | FLUSH_ARTICLE_ACTION_ADD = "add" 13 | FLUSH_ARTICLE_ACTION_UPDATE = "update" 14 | FLUSH_ARTICLE_ACTION_REMOVE = "remove" 15 | 16 | FLUSH_COMMENT_ACTION_ADD = "add" 17 | FLUSH_COMMENT_ACTION_UPDATE = "update" 18 | FLUSH_COMMENT_ACTION_REMOVE = "remove" 19 | 20 | ARTICLE_TYPE_DEFAULT_ID = 1 21 | -------------------------------------------------------------------------------- /model/site_info.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | 4 | class SiteCollection(object): 5 | title = None # string 6 | signature = None # string 7 | navbar = None # string 8 | menus = None # json(list) 9 | article_types_not_under_menu = None # 不在menu下的article_types #json(list) 10 | plugins = None # JSON(list) 11 | pv = None # int 12 | uv = None # int 13 | article_count = None # int 14 | comment_count = None # int 15 | article_sources = None # JSON(list) 16 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/anchor/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("anchor",function(a){function b(){var b=a.selection.getNode(),c="",d="A"==b.tagName&&""===a.dom.getAttrib(b,"href");d&&(c=b.name||b.id||""),a.windowManager.open({title:"Anchor",body:{type:"textbox",name:"name",size:40,label:"Name",value:c},onsubmit:function(c){var e=c.data.name;d?b.id=e:(a.selection.collapse(!0),a.execCommand("mceInsertContent",!1,a.dom.createHTML("a",{id:e})))}})}a.addCommand("mceAnchor",b),a.addButton("anchor",{icon:"anchor",tooltip:"Anchor",onclick:b,stateSelector:"a:not([href])"}),a.addMenuItem("anchor",{icon:"anchor",text:"Anchor",context:"insert",onclick:b})}); -------------------------------------------------------------------------------- /static/js/markdownEdit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mhq on 17/1/8. 3 | */ 4 | var markdown_reg = /[\\\`\*\_\[\]\#\+\-\!\>\s]/g; 5 | $(function () { 6 | $('.markdown-edit').markdown({ 7 | height:'500', 8 | language:'zh', 9 | footer:'字数: 0', 10 | onChange:function(e) { 11 | var content = e.getContent(); 12 | content_length = content.replace(markdown_reg, '').length; 13 | $('#markdown-counter').html(content_length); 14 | }, 15 | afterShowPreview: function (e) { 16 | codeHighLight(); 17 | } 18 | }) 19 | }); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/example/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("example",function(a,b){a.addButton("example",{text:"My button",icon:!1,onclick:function(){a.windowManager.open({title:"Example plugin",body:[{type:"textbox",name:"title",label:"Title"}],onsubmit:function(b){a.insertContent("Title: "+b.data.title)}})}}),a.addMenuItem("example",{text:"Example plugin",context:"tools",onclick:function(){a.windowManager.open({title:"TinyMCE site",url:b+"/dialog.html",width:600,height:400,buttons:[{text:"Insert",onclick:function(){var b=a.windowManager.getWindows()[0];a.insertContent(b.getContentWindow().document.getElementById("content").value),b.close()}},{text:"Close",onclick:"close"}]})}})}); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/nonbreaking/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("nonbreaking",function(a){var b=a.getParam("nonbreaking_force_tab");if(a.addCommand("mceNonBreaking",function(){a.insertContent(a.plugins.visualchars&&a.plugins.visualchars.state?' ':" "),a.dom.setAttrib(a.dom.select("span.mce-nbsp"),"data-mce-bogus","1")}),a.addButton("nonbreaking",{title:"Nonbreaking space",cmd:"mceNonBreaking"}),a.addMenuItem("nonbreaking",{text:"Nonbreaking space",cmd:"mceNonBreaking",context:"insert"}),b){var c=+b>1?+b:3;a.on("keydown",function(b){if(9==b.keyCode){if(b.shiftKey)return;b.preventDefault();for(var d=0;c>d;d++)a.execCommand("mceNonBreaking")}})}}); -------------------------------------------------------------------------------- /extends/time_task.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | from apscheduler.schedulers.tornado import TornadoScheduler 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class TimeTask(object): 9 | def __init__(self, sqlalchemy_engine): 10 | self.scheduler = TornadoScheduler() 11 | self.scheduler.add_jobstore("sqlalchemy", engine=sqlalchemy_engine) 12 | 13 | def add_cache_flush_task(self, func, *args, **kwargs): 14 | self.scheduler.add_job(func, 'cron', args=args, kwargs=kwargs, 15 | id="cache_flush", replace_existing=True, hour=0, day='*') 16 | return self 17 | 18 | def start_tasks(self): 19 | self.scheduler.start() 20 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/code/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("code",function(a){function b(){var b=a.windowManager.open({title:"Source code",body:{type:"textbox",name:"code",multiline:!0,minWidth:a.getParam("code_dialog_width",600),minHeight:a.getParam("code_dialog_height",Math.min(tinymce.DOM.getViewPort().h-200,500)),spellcheck:!1,style:"direction: ltr; text-align: left"},onSubmit:function(b){a.focus(),a.undoManager.transact(function(){a.setContent(b.data.code)}),a.selection.setCursorLocation(),a.nodeChanged()}});b.find("#code").value(a.getContent({source_view:!0}))}a.addCommand("mceCodeEditor",b),a.addButton("code",{icon:"code",tooltip:"Source code",onclick:b}),a.addMenuItem("code",{icon:"code",text:"Source code",context:"tools",onclick:b})}); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/directionality/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("directionality",function(a){function b(b){var c,d=a.dom,e=a.selection.getSelectedBlocks();e.length&&(c=d.getAttrib(e[0],"dir"),tinymce.each(e,function(a){d.getParent(a.parentNode,"*[dir='"+b+"']",d.getRoot())||(c!=b?d.setAttrib(a,"dir",b):d.setAttrib(a,"dir",null))}),a.nodeChanged())}function c(a){var b=[];return tinymce.each("h1 h2 h3 h4 h5 h6 div p".split(" "),function(c){b.push(c+"[dir="+a+"]")}),b.join(",")}a.addCommand("mceDirectionLTR",function(){b("ltr")}),a.addCommand("mceDirectionRTL",function(){b("rtl")}),a.addButton("ltr",{title:"Left to right",cmd:"mceDirectionLTR",stateSelector:c("ltr")}),a.addButton("rtl",{title:"Right to left",cmd:"mceDirectionRTL",stateSelector:c("rtl")})}); -------------------------------------------------------------------------------- /model/logined_user.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from extends.utils import Dict 3 | 4 | 5 | class LoginUser(Dict): 6 | # self['id'] = None 7 | # self['name'] = None 8 | # self['avatar'] = None 9 | # self['email'] = None 10 | 11 | def __init__(self, user): 12 | super(LoginUser, self).__init__() 13 | if isinstance(user, dict): 14 | self.update(user) 15 | 16 | # def add_message(self, message): 17 | # if 'messages' not in self: 18 | # self['messages'] = [message] 19 | # else: 20 | # self['messages'].append(message) 21 | # 22 | # def read_messages(self): 23 | # all_messages = self['messages'] 24 | # self['messages'] = None 25 | # return all_messages 26 | -------------------------------------------------------------------------------- /static/css/highlight/default.min.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:0.5em;background:#F0F0F0}.hljs,.hljs-subst{color:#444}.hljs-comment{color:#888888}.hljs-keyword,.hljs-attribute,.hljs-selector-tag,.hljs-meta-keyword,.hljs-doctag,.hljs-name{font-weight:bold}.hljs-type,.hljs-string,.hljs-number,.hljs-selector-id,.hljs-selector-class,.hljs-quote,.hljs-template-tag,.hljs-deletion{color:#880000}.hljs-title,.hljs-section{color:#880000;font-weight:bold}.hljs-regexp,.hljs-symbol,.hljs-variable,.hljs-template-variable,.hljs-link,.hljs-selector-attr,.hljs-selector-pseudo{color:#BC6060}.hljs-literal{color:#78A960}.hljs-built_in,.hljs-bullet,.hljs-code,.hljs-addition{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta-string{color:#4d99bf}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold} -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/contextmenu/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("contextmenu",function(a){var b,c=a.settings.contextmenu_never_use_native;a.on("contextmenu",function(d){var e,f=a.getDoc();if(!d.ctrlKey||c){if(d.preventDefault(),tinymce.Env.mac&&tinymce.Env.webkit&&2==d.button&&f.caretRangeFromPoint&&a.selection.setRng(f.caretRangeFromPoint(d.x,d.y)),e=a.settings.contextmenu||"link image inserttable | cell row column deletetable",b)b.show();else{var g=[];tinymce.each(e.split(/[ ,]/),function(b){var c=a.menuItems[b];"|"==b&&(c={text:b}),c&&(c.shortcut="",g.push(c))});for(var h=0;h 4 | */ 5 | (function ($) { 6 | $.fn.markdown.messages.zh = { 7 | 'Bold': "粗体", 8 | 'Italic': "斜体", 9 | 'Heading': "标题", 10 | 'URL/Link': "链接", 11 | 'Image': "图片", 12 | 'List': "列表", 13 | 'Unordered List': "无序列表", 14 | 'Ordered List': "有序列表", 15 | 'Code': "代码", 16 | 'Quote': "引用", 17 | 'Preview': "预览", 18 | 'strong text': "粗体", 19 | 'emphasized text': "强调", 20 | 'heading text': "标题", 21 | 'enter link description here': "输入链接说明", 22 | 'Insert Hyperlink': "URL地址", 23 | 'enter image description here': "输入图片说明", 24 | 'Insert Image Hyperlink': "图片URL地址", 25 | 'enter image title here': "在这里输入图片标题", 26 | 'list text here': "这里是列表文本", 27 | 'code text here': "这里输入代码", 28 | 'quote here': "这里输入引用文本" 29 | }; 30 | }(jQuery)); 31 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/emoticons/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("emoticons",function(a,b){function c(){var a;return a='',tinymce.each(d,function(c){a+="",tinymce.each(c,function(c){var d=b+"/img/smiley-"+c+".gif";a+=''}),a+=""}),a+="
"}var d=[["cool","cry","embarassed","foot-in-mouth"],["frown","innocent","kiss","laughing"],["money-mouth","sealed","smile","surprised"],["tongue-out","undecided","wink","yell"]];a.addButton("emoticons",{type:"panelbutton",panel:{role:"application",autohide:!0,html:c,onclick:function(b){var c=a.dom.getParent(b.target,"a");c&&(a.insertContent(''+c.getAttribute('),this.hide())}},tooltip:"Emoticons"})}); -------------------------------------------------------------------------------- /model/search_params/article_params.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | class ArticleSearchParams(object): 3 | 4 | ORDER_MODE_CREATE_TIME_DESC = 1 5 | 6 | def __init__(self, request): 7 | self.order_mode = request.get_argument("order_mode", ArticleSearchParams.ORDER_MODE_CREATE_TIME_DESC) 8 | self.source_id = request.get_argument("source_id", None) 9 | self.articleType_id = request.get_argument("articleType_id", None) 10 | self.show_source = True 11 | self.show_article_type = True 12 | self.show_summary = False 13 | self.show_content = False 14 | self.show_comments_count = False 15 | 16 | def to_url_params(self): 17 | s = "" 18 | if self.source_id: 19 | s = "source_id={0}".format(self.source_id) 20 | if self.articleType_id: 21 | if s: 22 | s += "&" 23 | s += "articleType_id={0}".format(self.articleType_id) 24 | return s -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/wordcount/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("wordcount",function(a){function b(){a.theme.panel.find("#wordcount").text(["Words: {0}",e.getCount()])}var c,d,e=this;c=a.getParam("wordcount_countregex",/[\w\u2019\x27\-\u00C0-\u1FFF]+/g),d=a.getParam("wordcount_cleanregex",/[0-9.(),;:!?%#$?\x27\x22_+=\\\/\-]*/g),a.on("init",function(){var c=a.theme.panel&&a.theme.panel.find("#statusbar")[0];c&&tinymce.util.Delay.setEditorTimeout(a,function(){c.insert({type:"label",name:"wordcount",text:["Words: {0}",e.getCount()],classes:"wordcount",disabled:a.settings.readonly},0),a.on("setcontent beforeaddundo",b),a.on("keyup",function(a){32==a.keyCode&&b()})},0)}),e.getCount=function(){var b=a.getContent({format:"raw"}),e=0;if(b){b=b.replace(/\.\.\./g," "),b=b.replace(/<.[^<>]*?>/g," ").replace(/ | /gi," "),b=b.replace(/(\w+)(&#?[a-z0-9]+;)+(\w+)/i,"$1$3").replace(/&.+?;/g," "),b=b.replace(d,"");var f=b.match(c);f&&(e=f.length)}return e}}); -------------------------------------------------------------------------------- /service/custom_service.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from model.models import BlogInfo 3 | 4 | """ 5 | 博客定制相关服务 6 | """ 7 | 8 | 9 | class BlogInfoService(object): 10 | 11 | @staticmethod 12 | def get_blog_info(db_session): 13 | blog = db_session.query(BlogInfo).first() 14 | return blog 15 | 16 | @staticmethod 17 | def update_blog_info(db_session, blog_info): 18 | blog_info_old = BlogInfoService.get_blog_info(db_session) 19 | if blog_info_old is not None: 20 | if "title" in blog_info and blog_info['title'] is not None: 21 | blog_info_old.title = blog_info['title'] 22 | if "signature" in blog_info and blog_info['signature'] is not None: 23 | blog_info_old.signature = blog_info['signature'] 24 | if "navbar" in blog_info and blog_info['navbar'] is not None: 25 | blog_info_old.navbar = blog_info['navbar'] 26 | db_session.commit() 27 | return blog_info_old 28 | -------------------------------------------------------------------------------- /service/blog_view_service.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | import datetime 4 | from model.models import BlogView 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class BlogViewService(object): 10 | @staticmethod 11 | def get_blog_view(db_session, date=None): 12 | if not date: 13 | date = datetime.date.today() 14 | blog_view = db_session.query(BlogView).get(date) 15 | return blog_view 16 | 17 | @staticmethod 18 | def add_blog_view(db_session, add_pv, add_uv, date=None): 19 | if not date: 20 | date = datetime.date.today() 21 | blog_view = BlogViewService.get_blog_view(db_session, date) 22 | if blog_view: 23 | blog_view.pv = BlogView.pv + add_pv 24 | blog_view.uv = BlogView.uv + add_uv 25 | else: 26 | blog_view = BlogView(date=date, pv=add_pv, uv=add_uv) 27 | db_session.add(blog_view) 28 | db_session.commit() 29 | return blog_view 30 | -------------------------------------------------------------------------------- /controller/super.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from tornado import gen 3 | 4 | from base import BaseHandler 5 | from service.user_service import UserService 6 | 7 | 8 | class SuperHandler(BaseHandler): 9 | @gen.coroutine 10 | def get(self): 11 | user_count = yield self.async_do(UserService.get_count, self.db) 12 | if not user_count: 13 | self.render("super/init.html") 14 | else: 15 | self.write_error(404) 16 | 17 | @gen.coroutine 18 | def post(self): 19 | user = dict( 20 | email=self.get_argument('email'), 21 | username=self.get_argument('username'), 22 | password=self.get_argument('password'), 23 | ) 24 | user_saved = yield self.async_do(UserService.save_user, self.db, user) 25 | if user_saved and user_saved.id: 26 | self.add_message('success', u'创建成功!') 27 | self.redirect(self.reverse_url('login')) 28 | else: 29 | self.add_message('danger', u'创建失败!') 30 | self.redirect(self.reverse_url('super.init')) 31 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/noneditable/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("noneditable",function(a){function b(a){return function(b){return-1!==(" "+b.attr("class")+" ").indexOf(a)}}function c(b){function c(b){var c=arguments,d=c[c.length-2];return d>0&&'"'==g.charAt(d-1)?b:''+a.dom.encode("string"==typeof c[1]?c[1]:c[0])+""}var d=f.length,g=b.content,h=tinymce.trim(e);if("raw"!=b.format){for(;d--;)g=g.replace(f[d],c);b.content=g}}var d,e,f,g="contenteditable";d=" "+tinymce.trim(a.getParam("noneditable_editable_class","mceEditable"))+" ",e=" "+tinymce.trim(a.getParam("noneditable_noneditable_class","mceNonEditable"))+" ";var h=b(d),i=b(e);f=a.getParam("noneditable_regexp"),f&&!f.length&&(f=[f]),a.on("PreInit",function(){f&&a.on("BeforeSetContent",c),a.parser.addAttributeFilter("class",function(a){for(var b,c=a.length;c--;)b=a[c],h(b)?b.attr(g,"true"):i(b)&&b.attr(g,"false")}),a.serializer.addAttributeFilter(g,function(a){for(var b,c=a.length;c--;)b=a[c],(h(b)||i(b))&&(f&&b.attr("data-mce-content")?(b.name="#text",b.type=3,b.raw=!0,b.value=b.attr("data-mce-content")):b.attr(g,null))})})}); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/save/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("save",function(a){function b(){var b;return b=tinymce.DOM.getParent(a.id,"form"),!a.getParam("save_enablewhendirty",!0)||a.isDirty()?(tinymce.triggerSave(),a.getParam("save_onsavecallback")?(a.execCallback("save_onsavecallback",a),void a.nodeChanged()):void(b?(a.setDirty(!1),(!b.onsubmit||b.onsubmit())&&("function"==typeof b.submit?b.submit():c(a.translate("Error: Form submit field collision."))),a.nodeChanged()):c(a.translate("Error: No form element found.")))):void 0}function c(b){a.notificationManager.open({text:b,type:"error"})}function d(){var b=tinymce.trim(a.startContent);return a.getParam("save_oncancelcallback")?void a.execCallback("save_oncancelcallback",a):(a.setContent(b),a.undoManager.clear(),void a.nodeChanged())}function e(){var b=this;a.on("nodeChange dirty",function(){b.disabled(a.getParam("save_enablewhendirty",!0)&&!a.isDirty())})}a.addCommand("mceSave",b),a.addCommand("mceCancel",d),a.addButton("save",{icon:"save",text:"Save",cmd:"mceSave",disabled:!0,onPostRender:e}),a.addButton("cancel",{text:"Cancel",icon:!1,cmd:"mceCancel",disabled:!0,onPostRender:e}),a.addShortcut("Meta+S","","mceSave")}); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/visualblocks/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("visualblocks",function(a,b){function c(){var b=this;b.active(f),a.on("VisualBlocks",function(){b.active(a.dom.hasClass(a.getBody(),"mce-visualblocks"))})}var d,e,f;window.NodeList&&(a.addCommand("mceVisualBlocks",function(){var c,g=a.dom;d||(d=g.uniqueId(),c=g.create("link",{id:d,rel:"stylesheet",href:b+"/css/visualblocks.css"}),a.getDoc().getElementsByTagName("head")[0].appendChild(c)),a.on("PreviewFormats AfterPreviewFormats",function(b){f&&g.toggleClass(a.getBody(),"mce-visualblocks","afterpreviewformats"==b.type)}),g.toggleClass(a.getBody(),"mce-visualblocks"),f=a.dom.hasClass(a.getBody(),"mce-visualblocks"),e&&e.active(g.hasClass(a.getBody(),"mce-visualblocks")),a.fire("VisualBlocks")}),a.addButton("visualblocks",{title:"Show blocks",cmd:"mceVisualBlocks",onPostRender:c}),a.addMenuItem("visualblocks",{text:"Show blocks",cmd:"mceVisualBlocks",onPostRender:c,selectable:!0,context:"view",prependToContext:!0}),a.on("init",function(){a.settings.visualblocks_default_state&&a.execCommand("mceVisualBlocks",!1,null,{skip_focus:!0})}),a.on("remove",function(){a.dom.removeClass(a.getBody(),"mce-visualblocks")}))}); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/colorpicker/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("colorpicker",function(a){function b(b,c){function d(a){var b=new tinymce.util.Color(a),c=b.toRgb();f.fromJSON({r:c.r,g:c.g,b:c.b,hex:b.toHex().substr(1)}),e(b.toHex())}function e(a){f.find("#preview")[0].getEl().style.background=a}var f=a.windowManager.open({title:"Color",items:{type:"container",layout:"flex",direction:"row",align:"stretch",padding:5,spacing:10,items:[{type:"colorpicker",value:c,onchange:function(){var a=this.rgb();f&&(f.find("#r").value(a.r),f.find("#g").value(a.g),f.find("#b").value(a.b),f.find("#hex").value(this.value().substr(1)),e(this.value()))}},{type:"form",padding:0,labelGap:5,defaults:{type:"textbox",size:7,value:"0",flex:1,spellcheck:!1,onchange:function(){var a,b,c=f.find("colorpicker")[0];return a=this.name(),b=this.value(),"hex"==a?(b="#"+b,d(b),void c.value(b)):(b={r:f.find("#r").value(),g:f.find("#g").value(),b:f.find("#b").value()},c.value(b),void d(b))}},items:[{name:"r",label:"R",autofocus:1},{name:"g",label:"G"},{name:"b",label:"B"},{name:"hex",label:"#",value:"000000"},{name:"preview",type:"container",border:1}]}]},onSubmit:function(){b("#"+this.toJSON().hex)}});d(c)}a.settings.color_picker_callback||(a.settings.color_picker_callback=b)}); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/pagebreak/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("pagebreak",function(a){var b="mce-pagebreak",c=a.getParam("pagebreak_separator",""),d=new RegExp(c.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,function(a){return"\\"+a}),"gi"),e='';a.addCommand("mcePageBreak",function(){a.settings.pagebreak_split_block?a.insertContent("

"+e+"

"):a.insertContent(e)}),a.addButton("pagebreak",{title:"Page break",cmd:"mcePageBreak"}),a.addMenuItem("pagebreak",{text:"Page break",icon:"pagebreak",cmd:"mcePageBreak",context:"insert"}),a.on("ResolveName",function(c){"IMG"==c.target.nodeName&&a.dom.hasClass(c.target,b)&&(c.name="pagebreak")}),a.on("click",function(c){c=c.target,"IMG"===c.nodeName&&a.dom.hasClass(c,b)&&a.selection.select(c)}),a.on("BeforeSetContent",function(a){a.content=a.content.replace(d,e)}),a.on("PreInit",function(){a.serializer.addNodeFilter("img",function(b){for(var d,e,f=b.length;f--;)if(d=b[f],e=d.attr("class"),e&&-1!==e.indexOf("mce-pagebreak")){var g=d.parent;if(a.schema.getBlockElements()[g.name]&&a.settings.pagebreak_split_block){g.type=3,g.value=c,g.raw=!0,d.remove();continue}d.type=3,d.value=c,d.raw=!0}})})}); -------------------------------------------------------------------------------- /template/admin/blog_plugin_add.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin_base.html' %} 2 | 3 | {% block title2 %} 4 | 添加插件 5 | {% end %} 6 | 7 | {% block admin_content %} 8 |
9 |

添加插件

10 |
11 |
12 |
13 | {% module xsrf_form_html() %} 14 |
15 | 16 | 17 | (可选) 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 | {% end %} 29 | 30 | {% block private_script %} 31 | 32 | 33 | {% end %} -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/visualchars/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("visualchars",function(a){function b(b){function c(a){return''+a+""}function f(){var a,b="";for(a in n)b+=a;return new RegExp("["+b+"]","g")}function g(){var a,b="";for(a in n)b&&(b+=","),b+="span.mce-"+n[a];return b}var h,i,j,k,l,m,n,o,p=a.getBody(),q=a.selection;if(n={"\xa0":"nbsp","\xad":"shy"},d=!d,e.state=d,a.fire("VisualChars",{state:d}),o=f(),b&&(m=q.getBookmark()),d)for(i=[],tinymce.walk(p,function(a){3==a.nodeType&&a.nodeValue&&o.test(a.nodeValue)&&i.push(a)},"childNodes"),j=0;j=0;j--)a.dom.remove(i[j],1);q.moveToBookmark(m)}function c(){var b=this;a.on("VisualChars",function(a){b.active(a.state)})}var d,e=this;a.addCommand("mceVisualChars",b),a.addButton("visualchars",{title:"Show invisible characters",cmd:"mceVisualChars",onPostRender:c}),a.addMenuItem("visualchars",{text:"Show invisible characters",cmd:"mceVisualChars",onPostRender:c,selectable:!0,context:"view",prependToContext:!0}),a.on("beforegetcontent",function(a){d&&"raw"!=a.format&&!a.draft&&(d=!0,b(!1))})}); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/tabfocus/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("tabfocus",function(a){function b(a){9!==a.keyCode||a.ctrlKey||a.altKey||a.metaKey||a.preventDefault()}function c(b){function c(c){function f(a){return"BODY"===a.nodeName||"hidden"!=a.type&&"none"!=a.style.display&&"hidden"!=a.style.visibility&&f(a.parentNode)}function i(a){return/INPUT|TEXTAREA|BUTTON/.test(a.tagName)&&tinymce.get(b.id)&&-1!=a.tabIndex&&f(a)}if(h=d.select(":input:enabled,*[tabindex]:not(iframe)"),e(h,function(b,c){return b.id==a.id?(g=c,!1):void 0}),c>0){for(j=g+1;j=0;j--)if(i(h[j]))return h[j];return null}var g,h,i,j;if(!(9!==b.keyCode||b.ctrlKey||b.altKey||b.metaKey||b.isDefaultPrevented())&&(i=f(a.getParam("tab_focus",a.getParam("tabfocus_elements",":prev,:next"))),1==i.length&&(i[1]=i[0],i[0]=":prev"),h=b.shiftKey?":prev"==i[0]?c(-1):d.get(i[0]):":next"==i[1]?c(1):d.get(i[1]))){var k=tinymce.get(h.id||h.name);h.id&&k?k.focus():tinymce.util.Delay.setTimeout(function(){tinymce.Env.webkit||window.focus(),h.focus()},10),b.preventDefault()}}var d=tinymce.DOM,e=tinymce.each,f=tinymce.explode;a.on("init",function(){a.inline&&tinymce.DOM.setAttrib(a.getBody(),"tabIndex",null),a.on("keyup",b),tinymce.Env.gecko?a.on("keypress keydown",c):a.on("keydown",c)})}); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/advlist/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("advlist",function(a){function b(a,b){var c=[];return tinymce.each(b.split(/[ ,]/),function(a){c.push({text:a.replace(/\-/g," ").replace(/\b\w/g,function(a){return a.toUpperCase()}),data:"default"==a?"":a})}),c}function c(b,c){a.undoManager.transact(function(){var d,e=a.dom,f=a.selection;d=e.getParent(f.getNode(),"ol,ul"),d&&d.nodeName==b&&c!==!1||a.execCommand("UL"==b?"InsertUnorderedList":"InsertOrderedList"),c=c===!1?g[b]:c,g[b]=c,d=e.getParent(f.getNode(),"ol,ul"),d&&(e.setStyle(d,"listStyleType",c?c:null),d.removeAttribute("data-mce-style")),a.focus()})}function d(b){var c=a.dom.getStyle(a.dom.getParent(a.selection.getNode(),"ol,ul"),"listStyleType")||"";b.control.items().each(function(a){a.active(a.settings.data===c)})}var e,f,g={};e=b("OL",a.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),f=b("UL",a.getParam("advlist_bullet_styles","default,circle,disc,square")),a.addButton("numlist",{type:"splitbutton",tooltip:"Numbered list",menu:e,onshow:d,onselect:function(a){c("OL",a.control.settings.data)},onclick:function(){c("OL",!1)}}),a.addButton("bullist",{type:"splitbutton",tooltip:"Bullet list",menu:f,onshow:d,onselect:function(a){c("UL",a.control.settings.data)},onclick:function(){c("UL",!1)}})}); -------------------------------------------------------------------------------- /template/admin/blog_plugin_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin_base.html' %} 2 | 3 | {% block title2 %} 4 | 修改插件 5 | {% end %} 6 | 7 | {% block admin_content %} 8 |
9 |

添加插件

10 |
11 |
12 |
13 | {% module xsrf_form_html() %} 14 |
15 | 16 | 17 | (可选) 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 | {% end %} 29 | 30 | {% block private_script %} 31 | 32 | 33 | {% end %} -------------------------------------------------------------------------------- /log_config.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | import logging.handlers 4 | import tornado.log 5 | 6 | FILE = dict( 7 | log_path="logs/log", # 末尾自动添加 @端口号.txt_日期 8 | when="D", # 以什么单位分割文件 9 | interval=1, # 以上面的时间单位,隔几个单位分割文件 10 | backupCount=30, # 保留多少历史记录文件 11 | fmt="%(asctime)s - %(name)s - %(filename)s[line:%(lineno)d] - %(levelname)s - %(message)s", 12 | ) 13 | 14 | 15 | def init(port, console_handler=False, file_handler=True, log_path=FILE['log_path'], base_level="INFO"): 16 | logger = logging.getLogger() 17 | logger.setLevel(base_level) 18 | # 配置控制台输出 19 | if console_handler: 20 | channel_console = logging.StreamHandler() 21 | channel_console.setFormatter(tornado.log.LogFormatter()) 22 | logger.addHandler(channel_console) 23 | # 配置文件输出 24 | if file_handler: 25 | if not log_path: 26 | log_path = FILE['log_path'] 27 | log_path = log_path+"@"+str(port)+".txt" 28 | formatter = logging.Formatter(FILE['fmt']); 29 | channel_file = logging.handlers.TimedRotatingFileHandler( 30 | filename=log_path, 31 | when=FILE['when'], 32 | interval=FILE['interval'], 33 | backupCount=FILE['backupCount']) 34 | channel_file.setFormatter(formatter) 35 | logger.addHandler(channel_file) -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/preview/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("preview",function(a){var b=a.settings,c=!tinymce.Env.ie;a.addCommand("mcePreview",function(){a.windowManager.open({title:"Preview",width:parseInt(a.getParam("plugin_preview_width","650"),10),height:parseInt(a.getParam("plugin_preview_height","500"),10),html:'",buttons:{text:"Close",onclick:function(){this.parent().parent().close()}},onPostRender:function(){var d,e="";e+='',tinymce.each(a.contentCSS,function(b){e+=''});var f=b.body_id||"tinymce";-1!=f.indexOf("=")&&(f=a.getParam("body_id","","hash"),f=f[a.id]||f);var g=b.body_class||"";-1!=g.indexOf("=")&&(g=a.getParam("body_class","","hash"),g=g[a.id]||"");var h=a.settings.directionality?' dir="'+a.settings.directionality+'"':"";if(d=""+e+'"+a.getContent()+"",c)this.getEl("body").firstChild.src="data:text/html;charset=utf-8,"+encodeURIComponent(d);else{var i=this.getEl("body").firstChild.contentWindow.document;i.open(),i.write(d),i.close()}}})}),a.addButton("preview",{title:"Preview",cmd:"mcePreview"}),a.addMenuItem("preview",{text:"Preview",cmd:"mcePreview",context:"view"})}); -------------------------------------------------------------------------------- /static/js/floatButton.js: -------------------------------------------------------------------------------- 1 | //JS For FloatButton to goTop, goBottom and refresh 2 | function getCookie(name) { 3 | var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); 4 | return r ? r[1] : undefined; 5 | } 6 | 7 | function getAlertHtml(category, message) { 8 | var s = '
'+ 9 | ''+ 10 | message+'
'; 11 | return s; 12 | } 13 | 14 | function alertXtg(category, message, timeout) { 15 | s = getAlertHtml(category, message); 16 | $('body').append(s); 17 | if(timeout) { 18 | setTimeout(function () { 19 | $('#fix-alert').remove(); 20 | }, timeout); 21 | } 22 | } 23 | 24 | function codeHighLight() { 25 | if(typeof(hljs) != "undefined" ) { 26 | $('pre code').each(function (i, block) { 27 | hljs.highlightBlock(block); 28 | }); 29 | } 30 | } 31 | 32 | $(document).ready(function(){ 33 | //$('#goTop').click(function(){ 34 | // $(window).scrollTop(0); 35 | //}); 36 | $('#goTop').click(function(){ 37 | $('html, body').animate({scrollTop: '0px'}, 800); 38 | }); 39 | $('#refresh').click(function(){ 40 | window.location.reload(); 41 | }); 42 | $('#goBottom').click(function(){ 43 | $('html, body').animate({scrollTop: $('.footer').offset().top}, 800); 44 | }); 45 | }); 46 | 47 | 48 | -------------------------------------------------------------------------------- /static/js/tinymce_setup.js: -------------------------------------------------------------------------------- 1 | //For submit articles 2 | tinymce.init({ 3 | selector: '#content', 4 | directionality:'ltr', 5 | language:'zh_CN', 6 | height:400, 7 | plugins: [ 8 | 'advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker', 9 | 'searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking', 10 | 'save table contextmenu directionality emoticons template paste textcolor', 11 | 'codesample', 12 | ], 13 | toolbar: 'insertfile undo redo | \ 14 | styleselect | \ 15 | bold italic | \ 16 | alignleft aligncenter alignright alignjustify | \ 17 | bullist numlist outdent indent | \ 18 | link image | \ 19 | print preview media fullpage | \ 20 | forecolor backcolor emoticons |\ 21 | codesample fontsizeselect fullscreen', 22 | fontsize_formats: '10pt 12pt 14pt 18pt 24pt 36pt', 23 | nonbreaking_force_tab: true 24 | }); 25 | 26 | //For add plugin 27 | tinymce.init({ 28 | selector: '#pluginContent', 29 | directionality:'ltr', 30 | language:'zh_CN', 31 | plugins: [ 32 | 'advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker', 33 | 'searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking', 34 | 'save table contextmenu directionality emoticons template paste textcolor', 35 | 'codesample', 36 | ], 37 | }); 38 | -------------------------------------------------------------------------------- /service/pubsub_service.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | import tornado.gen 4 | from extends.pub_sub_tornadis import PubSubTornadis 5 | from init_service import SiteCacheService 6 | from config import redis_pub_sub_channels 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class PubSubService(PubSubTornadis): 12 | 13 | def __init__(self, redis_pub_sub_config, application, loop=None): 14 | super(PubSubService, self).__init__(redis_pub_sub_config, loop) 15 | self.application = application 16 | self.db_pool = self.application.db_pool 17 | self.cache_manager = self.application.cache_manager 18 | self.thread_executor = self.application.thread_executor 19 | self.thread_do = self.thread_executor.submit 20 | self._db_session = None 21 | 22 | @property 23 | def db(self): 24 | if not self._db_session: 25 | self._db_session = self.application.db_pool() 26 | return self._db_session 27 | 28 | @tornado.gen.coroutine 29 | def first_do_after_subscribed(self): 30 | yield SiteCacheService.query_all(self.cache_manager, self.thread_do, self.db) 31 | 32 | @tornado.gen.coroutine 33 | def do_msg(self, msgs): 34 | logger.info("收到redis消息: " + str(msgs)) 35 | if len(msgs) >= 3: 36 | channel = msgs[1] 37 | core_msgs = msgs[2:] 38 | if channel == redis_pub_sub_channels['cache_message_channel']: 39 | yield SiteCacheService.update_by_sub_msg(core_msgs, self.cache_manager, self.thread_do, self.db) -------------------------------------------------------------------------------- /service/user_service.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from model.models import User 3 | 4 | 5 | class UserService(object): 6 | 7 | @staticmethod 8 | def get_user(db_session, username): 9 | return db_session.query(User).filter(User.username == username).first() 10 | 11 | @staticmethod 12 | def update_user_info(db_session, username, password, user): 13 | current_user = UserService.get_user(db_session, username) 14 | if current_user and current_user.password == password: 15 | if "username" in user: 16 | current_user.username = user['username'] 17 | if "email" in user: 18 | current_user.email = user['email'] 19 | db_session.commit() 20 | return current_user 21 | else: 22 | return None 23 | 24 | @staticmethod 25 | def update_password(db_session, username, old_password, new_password): 26 | count = db_session.query(User).filter(User.username == username, User.password == old_password)\ 27 | .update({"password":new_password}) 28 | db_session.commit() 29 | return count 30 | 31 | @staticmethod 32 | def get_count(db_session): 33 | return db_session.query(User).count() 34 | 35 | @staticmethod 36 | def save_user(db_session, user): 37 | user_to_save = User( 38 | email=user['email'], 39 | username=user['username'], 40 | password=user['password'], 41 | ) 42 | db_session.add(user_to_save) 43 | db_session.commit() 44 | return user_to_save -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/fullscreen/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("fullscreen",function(a){function b(){var a,b,c=window,d=document,e=d.body;return e.offsetWidth&&(a=e.offsetWidth,b=e.offsetHeight),c.innerWidth&&c.innerHeight&&(a=c.innerWidth,b=c.innerHeight),{w:a,h:b}}function c(){var a=tinymce.DOM.getViewPort();return{x:a.x,y:a.y}}function d(a){scrollTo(a.x,a.y)}function e(){function e(){m.setStyle(p,"height",b().h-(o.clientHeight-p.clientHeight))}var n,o,p,q,r=document.body,s=document.documentElement;l=!l,o=a.getContainer(),n=o.style,p=a.getContentAreaContainer().firstChild,q=p.style,l?(k=c(),f=q.width,g=q.height,q.width=q.height="100%",i=n.width,j=n.height,n.width=n.height="",m.addClass(r,"mce-fullscreen"),m.addClass(s,"mce-fullscreen"),m.addClass(o,"mce-fullscreen"),m.bind(window,"resize",e),e(),h=e):(q.width=f,q.height=g,i&&(n.width=i),j&&(n.height=j),m.removeClass(r,"mce-fullscreen"),m.removeClass(s,"mce-fullscreen"),m.removeClass(o,"mce-fullscreen"),m.unbind(window,"resize",h),d(k)),a.fire("FullscreenStateChanged",{state:l})}var f,g,h,i,j,k,l=!1,m=tinymce.DOM;return a.settings.inline?void 0:(a.on("init",function(){a.addShortcut("Meta+Alt+F","",e)}),a.on("remove",function(){h&&m.unbind(window,"resize",h)}),a.addCommand("mceFullScreen",e),a.addMenuItem("fullscreen",{text:"Fullscreen",shortcut:"Meta+Alt+F",selectable:!0,onClick:e,onPostRender:function(){var b=this;a.on("FullscreenStateChanged",function(a){b.active(a.state)})},context:"view"}),a.addButton("fullscreen",{tooltip:"Fullscreen",shortcut:"Meta+Alt+F",onClick:e,onPostRender:function(){var b=this;a.on("FullscreenStateChanged",function(a){b.active(a.state)})}}),{isFullscreen:function(){return l}})}); -------------------------------------------------------------------------------- /template/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if pager and pager.result %} 6 | {% for article in pager.result %} 7 |
8 |
9 |

10 | {{ article.title }} 11 |

12 |
13 | 32 |
33 |

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

34 |
35 |
36 | {% end %} 37 | {% end %} 38 | {% module Template("_macros.html", pager=pager, url=base_url, params=article_search_params.to_url_params()) %} 39 | 40 | {% end %} -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # max length of characters to apply to the 11 | # "slug" field 12 | #truncate_slug_length = 40 13 | 14 | # set to 'true' to run the environment during 15 | # the 'revision' command, regardless of autogenerate 16 | # revision_environment = false 17 | 18 | # set to 'true' to allow .pyc and .pyo files without 19 | # a source .py file to be detected as revisions in the 20 | # versions/ directory 21 | # sourceless = false 22 | 23 | # version location specification; this defaults 24 | # to alembic/versions. When using multiple version 25 | # directories, initial revisions must be specified with --version-path 26 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions 27 | 28 | # the output encoding used when revision files 29 | # are written from script.py.mako 30 | # output_encoding = utf-8 31 | 32 | sqlalchemy.url = driver://user:pass@localhost/dbname 33 | 34 | 35 | # Logging configuration 36 | [loggers] 37 | keys = root,sqlalchemy,alembic 38 | 39 | [handlers] 40 | keys = console 41 | 42 | [formatters] 43 | keys = generic 44 | 45 | [logger_root] 46 | level = WARN 47 | handlers = console 48 | qualname = 49 | 50 | [logger_sqlalchemy] 51 | level = WARN 52 | handlers = 53 | qualname = sqlalchemy.engine 54 | 55 | [logger_alembic] 56 | level = INFO 57 | handlers = 58 | qualname = alembic 59 | 60 | [handler_console] 61 | class = StreamHandler 62 | args = (sys.stderr,) 63 | level = NOTSET 64 | formatter = generic 65 | 66 | [formatter_generic] 67 | format = %(levelname)-5.5s [%(name)s] %(message)s 68 | datefmt = %H:%M:%S 69 | -------------------------------------------------------------------------------- /static/js/articleDetail.js: -------------------------------------------------------------------------------- 1 | function update_disable(url) { 2 | var _xsrf = getCookie("_xsrf"); 3 | $.post(url, {_xsrf:_xsrf}, function (data) { 4 | location.reload(); 5 | }); 6 | } 7 | 8 | function delete_comment(url) { 9 | var _xsrf = getCookie("_xsrf"); 10 | $.post(url, {_xsrf:_xsrf}, function (data) { 11 | location.reload(); 12 | }); 13 | } 14 | 15 | function delCommentCfm(url) { 16 | $('#delCommentCfmClick').click(function(){ 17 | delete_comment(url); 18 | }); 19 | $('#delCommentCfmModel').modal(); 20 | } 21 | 22 | function go_to_reply(comment_type, reply_to_id, reply_to_floor) { 23 | $('#reply-dialog-box').remove(); 24 | $('#submit-comment-form').prepend('
' + 25 | ''+ 26 | ''+ 27 | ''+ 28 | '' + 29 | '回复给' + reply_to_floor +'
'); 30 | $('html, body').animate({scrollTop:$('#submit-comment').offset().top}); 31 | } 32 | 33 | //Reset the follow value when refresh page 34 | window.onload = function(){ 35 | var content = $('.article-content').text(); 36 | $('.article-content').html(markdown.toHTML(content)); 37 | $('.article-loading').hide(); 38 | $('.article-content').show(); 39 | codeHighLight(); 40 | var scrollName = location.hash; 41 | if(scrollName) { 42 | $("body,html").animate({scrollTop: $(scrollName).offset().top}, "fast"); 43 | } 44 | } 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | #user xtg; 2 | worker_processes 2; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | use epoll; 10 | worker_connections 1024; 11 | } 12 | 13 | 14 | http { 15 | include /etc/nginx/mime.types; 16 | default_type application/octet-stream; 17 | 18 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 19 | '$status $body_bytes_sent "$http_referer" ' 20 | '"$http_user_agent" "$http_x_forwarded_for"'; 21 | 22 | access_log /var/log/nginx/access.log main; 23 | 24 | sendfile on; 25 | #tcp_nopush on; 26 | 27 | keepalive_timeout 65; 28 | 29 | #gzip on; 30 | 31 | upstream backend { 32 | server 127.0.0.1:8001; 33 | server 127.0.0.1:8002; 34 | } 35 | 36 | server { 37 | listen 80; 38 | 39 | location /static/ { 40 | root /home/xtg/blog-xtg; 41 | expires 1d; 42 | } 43 | 44 | location / { 45 | proxy_pass http://backend; 46 | proxy_redirect off; 47 | proxy_set_header Host $host; 48 | proxy_set_header X-Real-IP $remote_addr; 49 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 50 | client_max_body_size 10m; 51 | client_body_buffer_size 128k; 52 | proxy_connect_timeout 90; 53 | proxy_send_timeout 90; 54 | proxy_read_timeout 90; 55 | proxy_buffer_size 64k; 56 | proxy_buffers 32 32k; 57 | proxy_busy_buffers_size 128k; 58 | proxy_temp_file_write_size 128k; 59 | } 60 | } 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /model/pager.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from extends.utils import Dict 3 | 4 | 5 | class Pager(Dict): 6 | 7 | DEFAULT_PAGE_SIZE = 10 8 | 9 | def __init__(self, request): 10 | self.pageNo = int(request.get_argument("pageNo", 1)) 11 | self.pageSize = int(request.get_argument("pageSize", Pager.DEFAULT_PAGE_SIZE)) 12 | self.totalPage = 1 13 | self.totalCount = 0 14 | self.result = [] 15 | 16 | def build_query(self, query): 17 | limit = self.pageSize 18 | if self.pageNo < 0: 19 | self.pageNo = self.pageNo + self.totalPage + 1 20 | offset = (self.pageNo-1)*self.pageSize if self.pageNo > 0 else 0 21 | query = query.limit(limit).offset(offset) 22 | return query 23 | 24 | def set_total_count(self, count): 25 | self.totalCount = count 26 | if count > 0: 27 | self.totalPage = (count+self.pageSize-1) / self.pageSize 28 | 29 | def set_result(self, result): 30 | if result: 31 | self.result = result 32 | 33 | def has_prev(self): 34 | return self.pageNo > 1 35 | 36 | def has_next(self): 37 | return self.pageNo < self.totalPage 38 | 39 | def build_url(self, url, page_no, params): 40 | if '?' in url: 41 | parts = url.split('?', 1) 42 | url = parts[0] 43 | params = parts[1]+"&"+params 44 | if page_no < 1: 45 | page_no = 0 46 | if page_no > self.totalPage: 47 | page_no = self.totalPage 48 | url = "{0}?pageNo={1}".format(url, page_no) 49 | if self.pageSize != Pager.DEFAULT_PAGE_SIZE: 50 | url += "&pageSize={0}".format(self.pageSize) 51 | if params: 52 | if params.startswith("#"): 53 | url += params 54 | else: 55 | url += "&{0}".format(params) 56 | return url 57 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/autoresize/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("autoresize",function(a){function b(){return a.plugins.fullscreen&&a.plugins.fullscreen.isFullscreen()}function c(d){var g,h,i,j,k,l,m,n,o,p,q,r,s=tinymce.DOM;if(h=a.getDoc()){if(i=h.body,j=h.documentElement,k=e.autoresize_min_height,!i||d&&"setcontent"===d.type&&d.initial||b())return void(i&&j&&(i.style.overflowY="auto",j.style.overflowY="auto"));m=a.dom.getStyle(i,"margin-top",!0),n=a.dom.getStyle(i,"margin-bottom",!0),o=a.dom.getStyle(i,"padding-top",!0),p=a.dom.getStyle(i,"padding-bottom",!0),q=a.dom.getStyle(i,"border-top-width",!0),r=a.dom.getStyle(i,"border-bottom-width",!0),l=i.offsetHeight+parseInt(m,10)+parseInt(n,10)+parseInt(o,10)+parseInt(p,10)+parseInt(q,10)+parseInt(r,10),(isNaN(l)||0>=l)&&(l=tinymce.Env.ie?i.scrollHeight:tinymce.Env.webkit&&0===i.clientHeight?0:i.offsetHeight),l>e.autoresize_min_height&&(k=l),e.autoresize_max_height&&l>e.autoresize_max_height?(k=e.autoresize_max_height,i.style.overflowY="auto",j.style.overflowY="auto"):(i.style.overflowY="hidden",j.style.overflowY="hidden",i.scrollTop=0),k!==f&&(g=k-f,s.setStyle(a.iframeElement,"height",k+"px"),f=k,tinymce.isWebKit&&0>g&&c(d))}}function d(b,e,f){tinymce.util.Delay.setEditorTimeout(a,function(){c({}),b--?d(b,e,f):f&&f()},e)}var e=a.settings,f=0;a.settings.inline||(e.autoresize_min_height=parseInt(a.getParam("autoresize_min_height",a.getElement().offsetHeight),10),e.autoresize_max_height=parseInt(a.getParam("autoresize_max_height",0),10),a.on("init",function(){var b,c;b=a.getParam("autoresize_overflow_padding",1),c=a.getParam("autoresize_bottom_margin",50),b!==!1&&a.dom.setStyles(a.getBody(),{paddingLeft:b,paddingRight:b}),c!==!1&&a.dom.setStyles(a.getBody(),{paddingBottom:c})}),a.on("nodechange setcontent keyup FullscreenStateChanged",c),a.getParam("autoresize_on_init",!0)&&a.on("init",function(){d(20,100,function(){d(5,1e3)})}),a.addCommand("mceAutoResize",c))}); -------------------------------------------------------------------------------- /controller/admin.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from base import BaseHandler 3 | from tornado.gen import coroutine 4 | from tornado.web import authenticated 5 | from service.user_service import UserService 6 | 7 | 8 | class AdminAccountHandler(BaseHandler): 9 | 10 | @authenticated 11 | def get(self): 12 | self.render("admin/admin_account.html") 13 | 14 | @coroutine 15 | def post(self, require): 16 | if require == "edit-user-info": 17 | yield self.edit_user_info() 18 | elif require == "change-password": 19 | yield self.change_password() 20 | 21 | @authenticated 22 | @coroutine 23 | def edit_user_info(self): 24 | user_info = {"username": self.get_argument("username"), "email": self.get_argument("email")} 25 | user = yield self.async_do(UserService.update_user_info, self.db, self.current_user.name, 26 | self.get_argument("password"), user_info) 27 | if user: 28 | self.save_login_user(user) 29 | self.add_message('success', u'修改用户信息成功!') 30 | else: 31 | self.add_message('danger', u'修改用户信息失败!密码不正确!') 32 | self.redirect(self.reverse_url("admin.account")) 33 | 34 | @authenticated 35 | @coroutine 36 | def change_password(self): 37 | old_password = self.get_argument("old_password") 38 | new_password = self.get_argument("password") 39 | count = yield self.async_do(UserService.update_password, self.db, self.current_user.name, 40 | old_password, new_password) 41 | if count > 0: 42 | self.add_message('success', u'修改密码成功!') 43 | else: 44 | self.add_message('danger', u'修改密码失败!') 45 | self.redirect(self.reverse_url("admin.account")) 46 | 47 | 48 | class AdminHelpHandler(BaseHandler): 49 | 50 | @authenticated 51 | def get(self): 52 | self.render('admin/help_page.html') -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/autolink/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("autolink",function(a){function b(a){e(a,-1,"(",!0)}function c(a){e(a,0,"",!0)}function d(a){e(a,-1,"",!1)}function e(a,b,c){function d(a,b){if(0>b&&(b=0),3==a.nodeType){var c=a.data.length;b>c&&(b=c)}return b}function e(a,b){1!=a.nodeType||a.hasChildNodes()?h.setStart(a,d(a,b)):h.setStartBefore(a)}function f(a,b){1!=a.nodeType||a.hasChildNodes()?h.setEnd(a,d(a,b)):h.setEndAfter(a)}var h,i,j,k,l,m,n,o,p,q;if("A"!=a.selection.getNode().tagName){if(h=a.selection.getRng(!0).cloneRange(),h.startOffset<5){if(o=h.endContainer.previousSibling,!o){if(!h.endContainer.firstChild||!h.endContainer.firstChild.nextSibling)return;o=h.endContainer.firstChild.nextSibling}if(p=o.length,e(o,p),f(o,p),h.endOffset<5)return;i=h.endOffset,k=o}else{if(k=h.endContainer,3!=k.nodeType&&k.firstChild){for(;3!=k.nodeType&&k.firstChild;)k=k.firstChild;3==k.nodeType&&(e(k,0),f(k,k.nodeValue.length))}i=1==h.endOffset?2:h.endOffset-1-b}j=i;do e(k,i>=2?i-2:0),f(k,i>=1?i-1:0),i-=1,q=h.toString();while(" "!=q&&""!==q&&160!=q.charCodeAt(0)&&i-2>=0&&q!=c);h.toString()==c||160==h.toString().charCodeAt(0)?(e(k,i),f(k,j),i+=1):0===h.startOffset?(e(k,0),f(k,j)):(e(k,i),f(k,j)),m=h.toString(),"."==m.charAt(m.length-1)&&f(k,j-1),m=h.toString(),n=m.match(g),n&&("www."==n[1]?n[1]="http://www.":/@$/.test(n[1])&&!/^mailto:/.test(n[1])&&(n[1]="mailto:"+n[1]),l=a.selection.getBookmark(),a.selection.setRng(h),a.execCommand("createlink",!1,n[1]+n[2]),a.selection.moveToBookmark(l),a.nodeChanged())}}var f,g=/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i;return a.settings.autolink_pattern&&(g=a.settings.autolink_pattern),a.on("keydown",function(b){return 13==b.keyCode?d(a):void 0}),tinymce.Env.ie?void a.on("focus",function(){if(!f){f=!0;try{a.execCommand("AutoUrlDetect",!1,!0)}catch(b){}}}):(a.on("keypress",function(c){return 41==c.keyCode?b(a):void 0}),void a.on("keyup",function(b){return 32==b.keyCode?c(a):void 0}))}); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/insertdatetime/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("insertdatetime",function(a){function b(b,c){function d(a,b){if(a=""+a,a.length'+d+"";var f=a.dom.getParent(a.selection.getStart(),"time");if(f)return void a.dom.setOuterHTML(f,d)}a.insertContent(d)}var d,e,f="Sun Mon Tue Wed Thu Fri Sat Sun".split(" "),g="Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(" "),h="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),i="January February March April May June July August September October November December".split(" "),j=[];a.addCommand("mceInsertDate",function(){c(a.getParam("insertdatetime_dateformat",a.translate("%Y-%m-%d")))}),a.addCommand("mceInsertTime",function(){c(a.getParam("insertdatetime_timeformat",a.translate("%H:%M:%S")))}),a.addButton("insertdatetime",{type:"splitbutton",title:"Insert date/time",onclick:function(){c(d||e)},menu:j}),tinymce.each(a.settings.insertdatetime_formats||["%H:%M:%S","%Y-%m-%d","%I:%M:%S %p","%D"],function(a){e||(e=a),j.push({text:b(a),onclick:function(){d=a,c(a)}})}),a.addMenuItem("insertdatetime",{icon:"date",text:"Insert date/time",menu:j,context:"insert"})}); -------------------------------------------------------------------------------- /template/admin/help_page.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin_base.html' %} 2 | 3 | {% block title2 %} 4 | 帮助 5 | {% end %} 6 | 7 | {% block admin_content %} 8 |
9 |

blog_xtg帮助页面

10 |
11 |
一、blog_xtg简介
12 | 13 |

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

15 | 16 |

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

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

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

42 | 43 |

附:

44 | 45 | 51 |
52 | {% end %} -------------------------------------------------------------------------------- /extends/utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import json 3 | import logging 4 | from sqlalchemy.ext.declarative import DeclarativeMeta 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def singleton(cls, *args, **kw): 10 | instances = {} 11 | 12 | def _singleton(): 13 | if cls not in instances: 14 | instances[cls] = cls(*args, **kw) 15 | return instances[cls] 16 | return _singleton 17 | 18 | 19 | class AlchemyEncoder(json.JSONEncoder): 20 | 21 | def __init__(self, dumps_objs=None, *w, **kw): 22 | super(AlchemyEncoder, self).__init__(*w, **kw) 23 | if dumps_objs is None: 24 | dumps_objs = [] 25 | self.dumps_objs = dumps_objs 26 | 27 | def default(self, o): 28 | if isinstance(o.__class__, DeclarativeMeta): 29 | self.dumps_objs.append(o) 30 | data = {} 31 | fields = o.__json__() if hasattr(o, '__json__') else dir(o) 32 | fields = [f for f in fields if not f.startswith('_') and f not in ['metadata', 'query', 'query_class']] 33 | for field in fields: 34 | value = o.__getattribute__(field) 35 | if value and self.dumps_objs and value in self.dumps_objs: 36 | continue 37 | try: 38 | json.dumps(value, cls=AlchemyEncoder, dumps_objs=self.dumps_objs) 39 | data[field] = value 40 | except TypeError: 41 | pass 42 | return data 43 | return json.JSONEncoder.default(self, o) 44 | 45 | 46 | # 可通过 .attr 访问的dict 47 | class Dict(dict): 48 | def __getattr__(self, key): 49 | try: 50 | if isinstance(self[key], dict): 51 | return Dict(self[key]) 52 | return self[key] 53 | except KeyError: 54 | logger.warning(key+" not in "+str(self)) 55 | return None; 56 | 57 | def __setattr__(self, key, value): 58 | self[key] = value 59 | -------------------------------------------------------------------------------- /template/admin/admin_base.html: -------------------------------------------------------------------------------- 1 | {% extends '../base.html' %} 2 | 3 | {% block title %} 4 | blog_xtg - {% block title2 %}欢迎来到blog_xtg管理平台!{% end %} 5 | {% end %} 6 | 7 | {% block main %} 8 |
9 |
10 |
11 | 29 |
30 |
31 | {% if handler.has_message() %} 32 | {% for message in handler.read_messages() %} 33 |
34 | 35 | {{ message['message'] }} 36 |
37 | {% end %} 38 | {% end %} 39 | {% block admin_content %} 40 | {% end %} 41 |
42 |
43 |
44 | {% end %} 45 | 46 | {% block script %} 47 | 48 | {% block private_script %} 49 | {% end %} 50 | {% end %} 51 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/autosave/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce._beforeUnloadHandler=function(){var a;return tinymce.each(tinymce.editors,function(b){b.plugins.autosave&&b.plugins.autosave.storeDraft(),!a&&b.isDirty()&&b.getParam("autosave_ask_before_unload",!0)&&(a=b.translate("You have unsaved changes are you sure you want to navigate away?"))}),a},tinymce.PluginManager.add("autosave",function(a){function b(a,b){var c={s:1e3,m:6e4};return a=/^(\d+)([ms]?)$/.exec(""+(a||b)),(a[2]?c[a[2]]:1)*parseInt(a,10)}function c(){var a=parseInt(n.getItem(k+"time"),10)||0;return(new Date).getTime()-a>m.autosave_retention?(d(!1),!1):!0}function d(b){n.removeItem(k+"draft"),n.removeItem(k+"time"),b!==!1&&a.fire("RemoveDraft")}function e(){!j()&&a.isDirty()&&(n.setItem(k+"draft",a.getContent({format:"raw",no_events:!0})),n.setItem(k+"time",(new Date).getTime()),a.fire("StoreDraft"))}function f(){c()&&(a.setContent(n.getItem(k+"draft"),{format:"raw"}),a.fire("RestoreDraft"))}function g(){l||(setInterval(function(){a.removed||e()},m.autosave_interval),l=!0)}function h(){var b=this;b.disabled(!c()),a.on("StoreDraft RestoreDraft RemoveDraft",function(){b.disabled(!c())}),g()}function i(){a.undoManager.beforeChange(),f(),d(),a.undoManager.add()}function j(b){var c=a.settings.forced_root_block;return b=tinymce.trim("undefined"==typeof b?a.getBody().innerHTML:b),""===b||new RegExp("^<"+c+"[^>]*>((\xa0| |[ ]|]*>)+?|)|
$","i").test(b)}var k,l,m=a.settings,n=tinymce.util.LocalStorage;k=m.autosave_prefix||"tinymce-autosave-{path}{query}-{id}-",k=k.replace(/\{path\}/g,document.location.pathname),k=k.replace(/\{query\}/g,document.location.search),k=k.replace(/\{id\}/g,a.id),m.autosave_interval=b(m.autosave_interval,"30s"),m.autosave_retention=b(m.autosave_retention,"20m"),a.addButton("restoredraft",{title:"Restore last draft",onclick:i,onPostRender:h}),a.addMenuItem("restoredraft",{text:"Restore last draft",onclick:i,onPostRender:h,context:"file"}),a.settings.autosave_restore_when_empty!==!1&&(a.on("init",function(){c()&&j()&&f()}),a.on("saveContent",function(){d()})),window.onbeforeunload=tinymce._beforeUnloadHandler,this.hasDraft=c,this.storeDraft=e,this.restoreDraft=f,this.removeDraft=d,this.isEmpty=j}); -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from config import database_config 5 | from model.models import DbBase 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | config = context.config 10 | 11 | # add your model's MetaData object here 12 | # for 'autogenerate' support 13 | # from myapp import mymodel 14 | # target_metadata = mymodel.Base.metadata 15 | target_metadata = DbBase.metadata 16 | 17 | # other values from the config, defined by the needs of env.py, 18 | # can be acquired: 19 | # my_important_option = config.get_main_option("my_important_option") 20 | # ... etc. 21 | 22 | 23 | def run_migrations_offline(): 24 | """Run migrations in 'offline' mode. 25 | 26 | This configures the context with just a URL 27 | and not an Engine, though an Engine is acceptable 28 | here as well. By skipping the Engine creation 29 | we don't even need a DBAPI to be available. 30 | 31 | Calls to context.execute() here emit the given string to the 32 | script output. 33 | 34 | """ 35 | url = config.get_main_option("sqlalchemy.url") 36 | context.configure( 37 | url=url, target_metadata=target_metadata, literal_binds=True) 38 | 39 | with context.begin_transaction(): 40 | context.run_migrations() 41 | 42 | 43 | def run_migrations_online(): 44 | """Run migrations in 'online' mode. 45 | 46 | In this scenario we need to create an Engine 47 | and associate a connection with the context. 48 | 49 | """ 50 | connectable = engine_from_config( 51 | {'sqlalchemy.url': database_config['engine_url']}, 52 | prefix='sqlalchemy.', 53 | poolclass=pool.NullPool) 54 | 55 | with connectable.connect() as connection: 56 | context.configure( 57 | connection=connection, 58 | target_metadata=target_metadata 59 | ) 60 | 61 | with context.begin_transaction(): 62 | context.run_migrations() 63 | 64 | if context.is_offline_mode(): 65 | run_migrations_offline() 66 | else: 67 | run_migrations_online() 68 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/importcss/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("importcss",function(a){function b(a){var b=tinymce.Env.cacheSuffix;return"string"==typeof a&&(a=a.replace("?"+b,"").replace("&"+b,"")),a}function c(b){var c=a.settings,d=c.skin!==!1?c.skin||"lightgray":!1;if(d){var e=c.skin_url;return e=e?a.documentBaseURI.toAbsolute(e):tinymce.baseURL+"/skins/"+d,b===e+"/content"+(a.inline?".inline":"")+".min.css"}return!1}function d(a){return"string"==typeof a?function(b){return-1!==b.indexOf(a)}:a instanceof RegExp?function(b){return a.test(b)}:a}function e(d,e){function f(a,d){var i,j=a.href;if(j=b(j),j&&e(j,d)&&!c(j)){h(a.imports,function(a){f(a,!0)});try{i=a.cssRules||a.rules}catch(k){}h(i,function(a){a.styleSheet?f(a.styleSheet,!0):a.selectorText&&h(a.selectorText.split(","),function(a){g.push(tinymce.trim(a))})})}}var g=[],i={};h(a.contentCSS,function(a){i[a]=!0}),e||(e=function(a,b){return b||i[a]});try{h(d.styleSheets,function(a){f(a)})}catch(j){}return g}function f(b){var c,d=/^(?:([a-z0-9\-_]+))?(\.[a-z0-9_\-\.]+)$/i.exec(b);if(d){var e=d[1],f=d[2].substr(1).split(".").join(" "),g=tinymce.makeMap("a,img");return d[1]?(c={title:b},a.schema.getTextBlockElements()[e]?c.block=e:a.schema.getBlockElements()[e]||g[e.toLowerCase()]?c.selector=e:c.inline=e):d[2]&&(c={inline:"span",title:b.substr(1),classes:f}),a.settings.importcss_merge_classes!==!1?c.classes=f:c.attributes={"class":f},c}}var g=this,h=tinymce.each;a.on("renderFormatsMenu",function(b){var c=a.settings,i={},j=c.importcss_selector_converter||f,k=d(c.importcss_selector_filter),l=b.control;a.settings.importcss_append||l.items().remove();var m=[];tinymce.each(c.importcss_groups,function(a){a=tinymce.extend({},a),a.filter=d(a.filter),m.push(a)}),h(e(b.doc||a.getDoc(),d(c.importcss_file_filter)),function(b){if(-1===b.indexOf(".mce-")&&!i[b]&&(!k||k(b))){var c,d=j.call(g,b);if(d){var e=d.name||tinymce.DOM.uniqueId();if(m)for(var f=0;f.md-header{display:block;padding:6px 4px;background:#f5f5f5}.md-editor>.md-header{margin:0}.md-editor>.md-preview{background:#fff;border-top:1px dashed #ddd;border-bottom:1px dashed #ddd;min-height:10px;overflow:auto}.md-editor>textarea{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;font-size:14px;outline:0;margin:0;display:block;padding:0;width:100%;border:0;border-top:1px dashed #ddd;border-bottom:1px dashed #ddd;border-radius:0;box-shadow:none;background:#eee}.md-editor>textarea:focus{box-shadow:none;background:#fff}.md-editor.active{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)}.md-editor .md-controls{float:right;padding:3px}.md-editor .md-controls .md-control{right:5px;color:#bebebe;padding:3px 3px 3px 10px}.md-editor .md-controls .md-control:hover{color:#333}.md-editor.md-fullscreen-mode{width:100%;height:100%;position:fixed;top:0;left:0;z-index:99999;padding:60px 30px 15px;background:#fff!important;border:0!important}.md-editor.md-fullscreen-mode .md-footer{display:none}.md-editor.md-fullscreen-mode .md-input,.md-editor.md-fullscreen-mode .md-preview{margin:0 auto!important;height:100%!important;font-size:20px!important;padding:20px!important;color:#999;line-height:1.6em!important;resize:none!important;box-shadow:none!important;background:#fff!important;border:0!important}.md-editor.md-fullscreen-mode .md-preview{color:#333;overflow:auto}.md-editor.md-fullscreen-mode .md-input:focus,.md-editor.md-fullscreen-mode .md-input:hover{color:#333;background:#fff!important}.md-editor.md-fullscreen-mode .md-header{background:0 0;text-align:center;position:fixed;width:100%;top:20px}.md-editor.md-fullscreen-mode .btn-group{float:none}.md-editor.md-fullscreen-mode .btn{border:0;background:0 0;color:#b3b3b3}.md-editor.md-fullscreen-mode .btn.active,.md-editor.md-fullscreen-mode .btn:active,.md-editor.md-fullscreen-mode .btn:focus,.md-editor.md-fullscreen-mode .btn:hover{box-shadow:none;color:#333}.md-editor.md-fullscreen-mode .md-fullscreen-controls{position:absolute;top:20px;right:20px;text-align:right;z-index:1002;display:block}.md-editor.md-fullscreen-mode .md-fullscreen-controls a{color:#b3b3b3;clear:right;margin:10px;width:30px;height:30px;text-align:center}.md-editor.md-fullscreen-mode .md-fullscreen-controls a:hover{color:#333;text-decoration:none}.md-editor.md-fullscreen-mode .md-editor{height:100%!important;position:relative}.md-editor .md-fullscreen-controls{display:none}.md-nooverflow{overflow:hidden;position:fixed;width:100%} -------------------------------------------------------------------------------- /template/super/init.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 创建管理员账户 6 | 7 | 8 | 9 | 10 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/textpattern/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("textpattern",function(a){function b(){return j&&(i.sort(function(a,b){return a.start.length>b.start.length?-1:a.start.length 2 |
    3 |
  • 4 | 5 | « 6 | 7 |
  • 8 | 9 | {% if pager.totalPage <= 8 %} 10 | {% for p in range(1, pager.totalPage+1) %} 11 |
  • 12 | {{ p }} 13 |
  • 14 | {% end %} 15 | {% else %} 16 | {% if pager.pageNo <= 5 %} 17 | {% for p in range(1, 6) %} 18 |
  • 19 | {{ p }} 20 |
  • 21 | {% end %} 22 | {% else %} 23 | {% for p in range(1, 3) %} 24 |
  • 25 | {{ p }} 26 |
  • 27 | {% end %} 28 |
  • 29 | {% for p in range(pager.pageNo-2, pager.pageNo+1) %} 30 |
  • 31 | {{ p }} 32 |
  • 33 | {% end %} 34 | {% end %} 35 | 36 | {% if pager.pageNo >= pager.totalPage-3 %} 37 | {% for p in range(pager.pageNo+1, pager.totalPage+1) %} 38 |
  • 39 | {{ p }} 40 |
  • 41 | {% end %} 42 | {% else %} 43 | {% for p in range(pager.pageNo+1 if pager.pageNo > 5 else 6, pager.pageNo+3) %} 44 |
  • 45 | {{ p }} 46 |
  • 47 | {% end %} 48 |
  • 49 |
  • 50 | {{ pager.totalPage }} 51 |
  • 52 | {% end %} 53 | {% end %} 54 |
  • 55 | 56 | » 57 | 58 |
  • 59 |
60 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/layer/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("layer",function(a){function b(a){do if(a.className&&-1!=a.className.indexOf("mceItemLayer"))return a;while(a=a.parentNode)}function c(b){var c=a.dom;tinymce.each(c.select("div,p",b),function(a){/^(absolute|relative|fixed)$/i.test(a.style.position)&&(a.hasVisual?c.addClass(a,"mceItemVisualAid"):c.removeClass(a,"mceItemVisualAid"),c.addClass(a,"mceItemLayer"))})}function d(c){var d,e,f=[],g=b(a.selection.getNode()),h=-1,i=-1;for(e=[],tinymce.walk(a.getBody(),function(a){1==a.nodeType&&/^(absolute|relative|static)$/i.test(a.style.position)&&e.push(a)},"childNodes"),d=0;dh&&e[d]==g&&(h=d);if(0>c){for(d=0;d-1?(e[h].style.zIndex=f[i],e[i].style.zIndex=f[h]):f[h]>0&&(e[h].style.zIndex=f[h]-1)}else{for(d=0;df[h]){i=d;break}i>-1?(e[h].style.zIndex=f[i],e[i].style.zIndex=f[h]):e[h].style.zIndex=f[h]+1}a.execCommand("mceRepaint")}function e(){var b=a.dom,c=b.getPos(b.getParent(a.selection.getNode(),"*")),d=a.getBody();a.dom.add(d,"div",{style:{position:"absolute",left:c.x,top:c.y>20?c.y:20,width:100,height:100},"class":"mceItemVisualAid mceItemLayer"},a.selection.getContent()||a.getLang("layer.content")),tinymce.Env.ie&&b.setHTML(d,d.innerHTML)}function f(){var c=b(a.selection.getNode());c||(c=a.dom.getParent(a.selection.getNode(),"DIV,P,IMG")),c&&("absolute"==c.style.position.toLowerCase()?(a.dom.setStyles(c,{position:"",left:"",top:"",width:"",height:""}),a.dom.removeClass(c,"mceItemVisualAid"),a.dom.removeClass(c,"mceItemLayer")):(c.style.left||(c.style.left="20px"),c.style.top||(c.style.top="20px"),c.style.width||(c.style.width=c.width?c.width+"px":"100px"),c.style.height||(c.style.height=c.height?c.height+"px":"100px"),c.style.position="absolute",a.dom.setAttrib(c,"data-mce-style",""),a.addVisual(a.getBody())),a.execCommand("mceRepaint"),a.nodeChanged())}a.addCommand("mceInsertLayer",e),a.addCommand("mceMoveForward",function(){d(1)}),a.addCommand("mceMoveBackward",function(){d(-1)}),a.addCommand("mceMakeAbsolute",function(){f()}),a.addButton("moveforward",{title:"layer.forward_desc",cmd:"mceMoveForward"}),a.addButton("movebackward",{title:"layer.backward_desc",cmd:"mceMoveBackward"}),a.addButton("absolute",{title:"layer.absolute_desc",cmd:"mceMakeAbsolute"}),a.addButton("insertlayer",{title:"layer.insertlayer_desc",cmd:"mceInsertLayer"}),a.on("init",function(){tinymce.Env.ie&&a.getDoc().execCommand("2D-Position",!1,!0)}),a.on("mouseup",function(c){var d=b(c.target);d&&a.dom.setAttrib(d,"data-mce-style","")}),a.on("mousedown",function(c){var d,e=c.target,f=a.getDoc();tinymce.Env.gecko&&(b(e)?"on"!==f.designMode&&(f.designMode="on",e=f.body,d=e.parentNode,d.removeChild(e),d.appendChild(e)):"on"==f.designMode&&(f.designMode="off"))}),a.on("NodeChange",c)}); -------------------------------------------------------------------------------- /template/auth/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 登陆blog_mhq管理后台 6 | 7 | 8 | 9 | 10 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/myskin/skin.json: -------------------------------------------------------------------------------- 1 | { 2 | "skin-name": "myskin", 3 | "preview-bg": "#666666", 4 | "text": "#b5b9bf", 5 | "text-inverse": "#000000", 6 | "text-disabled": "#6e737a", 7 | "has-gradients": true, 8 | "has-radius": true, 9 | "has-boxshadow": true, 10 | "has-button-borders": true, 11 | "btn-text": "#b5b9bf", 12 | "btn-text-shadow": "#000000", 13 | "btn-bg": "#515c67", 14 | "btn-bg-hlight": "#454f59", 15 | "btn-border-top": "rgba(32,42,51,1)", 16 | "btn-border-right": "rgba(32,42,51,1)", 17 | "btn-border-bottom": "rgba(32,42,51,1)", 18 | "btn-border-left": "rgba(32,42,51,1)", 19 | "btn-split-border": "#202a33", 20 | "btn-primary-text": "#ffffff", 21 | "btn-primary-text-shadow": "#333333", 22 | "btn-primary-bg": "#006fa6", 23 | "btn-primary-bg-hlight": "#005580", 24 | "btn-padding": "4px 10px", 25 | "menu-bg": "#2f3740", 26 | "menu-border": "#202a33", 27 | "menuitem-text": "#dddddd", 28 | "menuitem-bg-selected": "#006fa6", 29 | "menuitem-bg-selected-hlight": "#005580", 30 | "menuitem-separator-top": "#25313f", 31 | "menuitem-separator-bottom": "#424f5f", 32 | "menuitem-text-inverse": "#ffffff", 33 | "menuitem-bg-active": "#0085c7", 34 | "menuitem-text-active": "#ffffff", 35 | "menuitem-preview-border-active": "#08608c", 36 | "menubar-menubtn-text": "#b5b9bf", 37 | "checkbox-border": "#202a33", 38 | "checkbox-border-focus": "#1e7dad", 39 | "panel-border": "#232b33", 40 | "panel-bg": "#404952", 41 | "panel-bg-hlight": "#404952", 42 | "textbox-bg": "#515c67", 43 | "textbox-border": "#202a33", 44 | "textbox-border-focus": "#1e7dad", 45 | "window-bg": "#404952", 46 | "window-border": "#9e9e9e", 47 | "tab-bg": "#303942", 48 | "tab-bg-hover": "#404952", 49 | "tab-bg-active": "#404952", 50 | "tab-border": "#202a33", 51 | "tabs-bg": "#303942", 52 | "notification-bg": "#f0f0f0", 53 | "notification-border": "#cccccc", 54 | "notification-text": "#333333", 55 | "notification-success-bg": "#dff0d8", 56 | "notification-success-border": "#d6e9c6", 57 | "notification-success-text": "#3c763d", 58 | "notification-info-bg": "#d9edf7", 59 | "notification-info-border": "#779ecb", 60 | "notification-info-text": "#31708f", 61 | "notification-warning-bg": "#fcf8e3", 62 | "notification-warning-border": "#faebcc", 63 | "notification-warning-text": "#8a6d3b", 64 | "notification-error-bg": "#f2dede", 65 | "notification-error-border": "#ebccd1", 66 | "notification-error-text": "#a94442", 67 | "progress-bar-bg": "#515c67", 68 | "progress-bar-bg-hlight": "#515c67", 69 | "progress-border": "#202a33", 70 | "progress-text": "#c4c4c4", 71 | "progress-text-shadow": "#000000", 72 | "slider-bg": "#515c67", 73 | "slider-border": "#202a33", 74 | "slider-handle-bg": "#454f59", 75 | "slider-handle-border": "#000000", 76 | "colorbtn-backcolor-bg": "#384552", 77 | "grid-border": "#d6d6d6", 78 | "grid-border-active": "#d6d6d6" 79 | } -------------------------------------------------------------------------------- /template/admin/submit_articles.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin_base.html' %} 2 | 3 | {% block title2 %} 4 | 发表博文 5 | {% end %} 6 | 7 | {% block private_stylesheet %} 8 | 9 | 10 | {% end %} 11 | 12 | {% block admin_content %} 13 |
14 |

发表博文

15 |
16 |
17 | {% module xsrf_form_html() %} 18 |
19 | : 20 | 25 | (1-50字) 27 |
28 |
29 | : 30 | 35 |
36 |
37 | 38 |
39 |
40 | (显示在博客首页,不填的话,默认取文章前120个字)
41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 | {% end %} 49 | 50 | {% block private_script %} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {% end %} -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/skins/lightgray/content.min.css: -------------------------------------------------------------------------------- 1 | body{background-color:#fff;color:#000;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:11px;scrollbar-3dlight-color:#f0f0ee;scrollbar-arrow-color:#676662;scrollbar-base-color:#f0f0ee;scrollbar-darkshadow-color:#ddd;scrollbar-face-color:#e0e0dd;scrollbar-highlight-color:#f0f0ee;scrollbar-shadow-color:#f0f0ee;scrollbar-track-color:#f5f5f5}td,th{font-family:Verdana,Arial,Helvetica,sans-serif;font-size:11px}.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3a3a3a;background:#d5d5d5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-preview-object[data-mce-selected] .mce-shim{display:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3a3a3a;background:#d5d5d5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#aaa}.mce-shy::after{content:'-'}hr{cursor:default}.mce-match-marker{background:#aaa;color:#fff}.mce-match-marker-selected{background:#39f;color:#fff}.mce-spellchecker-word{border-bottom:2px solid #f00;cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #bbb}td[data-mce-selected],th[data-mce-selected]{background-color:#39f !important}.mce-edit-focus{outline:1px dotted #333}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2d8ac7}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #7acaff}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2d8ac7}.mce-resize-bar-dragging{background-color:blue;opacity:.25;filter:alpha(opacity=25);zoom:1} -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/codesample/css/prism.css: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript */ 2 | /** 3 | * prism.js default theme for JavaScript, CSS and HTML 4 | * Based on dabblet (http://dabblet.com) 5 | * @author Lea Verou 6 | */ 7 | 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: black; 11 | text-shadow: 0 1px white; 12 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 13 | direction: ltr; 14 | text-align: left; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | word-wrap: normal; 19 | line-height: 1.5; 20 | 21 | -moz-tab-size: 4; 22 | -o-tab-size: 4; 23 | tab-size: 4; 24 | 25 | -webkit-hyphens: none; 26 | -moz-hyphens: none; 27 | -ms-hyphens: none; 28 | hyphens: none; 29 | } 30 | 31 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 32 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 33 | text-shadow: none; 34 | background: #b3d4fc; 35 | } 36 | 37 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 38 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 39 | text-shadow: none; 40 | background: #b3d4fc; 41 | } 42 | 43 | @media print { 44 | code[class*="language-"], 45 | pre[class*="language-"] { 46 | text-shadow: none; 47 | } 48 | } 49 | 50 | /* Code blocks */ 51 | pre[class*="language-"] { 52 | padding: 1em; 53 | margin: .5em 0; 54 | overflow: auto; 55 | } 56 | 57 | :not(pre) > code[class*="language-"], 58 | pre[class*="language-"] { 59 | background: #f5f2f0; 60 | } 61 | 62 | /* Inline code */ 63 | :not(pre) > code[class*="language-"] { 64 | padding: .1em; 65 | border-radius: .3em; 66 | } 67 | 68 | .token.comment, 69 | .token.prolog, 70 | .token.doctype, 71 | .token.cdata { 72 | color: slategray; 73 | } 74 | 75 | .token.punctuation { 76 | color: #999; 77 | } 78 | 79 | .namespace { 80 | opacity: .7; 81 | } 82 | 83 | .token.property, 84 | .token.tag, 85 | .token.boolean, 86 | .token.number, 87 | .token.constant, 88 | .token.symbol, 89 | .token.deleted { 90 | color: #905; 91 | } 92 | 93 | .token.selector, 94 | .token.attr-name, 95 | .token.string, 96 | .token.char, 97 | .token.builtin, 98 | .token.inserted { 99 | color: #690; 100 | } 101 | 102 | .token.operator, 103 | .token.entity, 104 | .token.url, 105 | .language-css .token.string, 106 | .style .token.string { 107 | color: #a67f59; 108 | background: hsla(0, 0%, 100%, .5); 109 | } 110 | 111 | .token.atrule, 112 | .token.attr-value, 113 | .token.keyword { 114 | color: #07a; 115 | } 116 | 117 | .token.function { 118 | color: #DD4A68; 119 | } 120 | 121 | .token.regex, 122 | .token.important, 123 | .token.variable { 124 | color: #e90; 125 | } 126 | 127 | .token.important, 128 | .token.bold { 129 | font-weight: bold; 130 | } 131 | .token.italic { 132 | font-style: italic; 133 | } 134 | 135 | .token.entity { 136 | cursor: help; 137 | } 138 | 139 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/bbcode/plugin.min.js: -------------------------------------------------------------------------------- 1 | !function(){tinymce.create("tinymce.plugins.BBCodePlugin",{init:function(a){var b=this,c=a.getParam("bbcode_dialect","punbb").toLowerCase();a.on("beforeSetContent",function(a){a.content=b["_"+c+"_bbcode2html"](a.content)}),a.on("postProcess",function(a){a.set&&(a.content=b["_"+c+"_bbcode2html"](a.content)),a.get&&(a.content=b["_"+c+"_html2bbcode"](a.content))})},getInfo:function(){return{longname:"BBCode Plugin",author:"Ephox Corp",authorurl:"http://www.tinymce.com",infourl:"http://www.tinymce.com/wiki.php/Plugin:bbcode"}},_punbb_html2bbcode:function(a){function b(b,c){a=a.replace(b,c)}return a=tinymce.trim(a),b(/(.*?)<\/a>/gi,"[url=$1]$2[/url]"),b(/(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),b(/(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),b(/(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),b(/(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),b(/(.*?)<\/span>/gi,"[color=$1]$2[/color]"),b(/(.*?)<\/font>/gi,"[color=$1]$2[/color]"),b(/(.*?)<\/span>/gi,"[size=$1]$2[/size]"),b(/(.*?)<\/font>/gi,"$1"),b(//gi,"[img]$1[/img]"),b(/(.*?)<\/span>/gi,"[code]$1[/code]"),b(/(.*?)<\/span>/gi,"[quote]$1[/quote]"),b(/(.*?)<\/strong>/gi,"[code][b]$1[/b][/code]"),b(/(.*?)<\/strong>/gi,"[quote][b]$1[/b][/quote]"),b(/(.*?)<\/em>/gi,"[code][i]$1[/i][/code]"),b(/(.*?)<\/em>/gi,"[quote][i]$1[/i][/quote]"),b(/(.*?)<\/u>/gi,"[code][u]$1[/u][/code]"),b(/(.*?)<\/u>/gi,"[quote][u]$1[/u][/quote]"),b(/<\/(strong|b)>/gi,"[/b]"),b(/<(strong|b)>/gi,"[b]"),b(/<\/(em|i)>/gi,"[/i]"),b(/<(em|i)>/gi,"[i]"),b(/<\/u>/gi,"[/u]"),b(/(.*?)<\/span>/gi,"[u]$1[/u]"),b(//gi,"[u]"),b(/]*>/gi,"[quote]"),b(/<\/blockquote>/gi,"[/quote]"),b(/
/gi,"\n"),b(//gi,"\n"),b(/
/gi,"\n"),b(/

/gi,""),b(/<\/p>/gi,"\n"),b(/ |\u00a0/gi," "),b(/"/gi,'"'),b(/</gi,"<"),b(/>/gi,">"),b(/&/gi,"&"),a},_punbb_bbcode2html:function(a){function b(b,c){a=a.replace(b,c)}return a=tinymce.trim(a),b(/\n/gi,"
"),b(/\[b\]/gi,""),b(/\[\/b\]/gi,""),b(/\[i\]/gi,""),b(/\[\/i\]/gi,""),b(/\[u\]/gi,""),b(/\[\/u\]/gi,""),b(/\[url=([^\]]+)\](.*?)\[\/url\]/gi,'$2'),b(/\[url\](.*?)\[\/url\]/gi,'$1'),b(/\[img\](.*?)\[\/img\]/gi,''),b(/\[color=(.*?)\](.*?)\[\/color\]/gi,'$2'),b(/\[code\](.*?)\[\/code\]/gi,'$1 '),b(/\[quote.*?\](.*?)\[\/quote\]/gi,'$1 '),a}}),tinymce.PluginManager.add("bbcode",tinymce.plugins.BBCodePlugin)}(); -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/legacyoutput/plugin.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.on("AddEditor",function(a){a.editor.settings.inline_styles=!1}),a.PluginManager.add("legacyoutput",function(b,c,d){b.on("init",function(){var c="p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img",d=a.explode(b.settings.font_size_style_values),e=b.schema;b.formatter.register({alignleft:{selector:c,attributes:{align:"left"}},aligncenter:{selector:c,attributes:{align:"center"}},alignright:{selector:c,attributes:{align:"right"}},alignjustify:{selector:c,attributes:{align:"justify"}},bold:[{inline:"b",remove:"all"},{inline:"strong",remove:"all"},{inline:"span",styles:{fontWeight:"bold"}}],italic:[{inline:"i",remove:"all"},{inline:"em",remove:"all"},{inline:"span",styles:{fontStyle:"italic"}}],underline:[{inline:"u",remove:"all"},{inline:"span",styles:{textDecoration:"underline"},exact:!0}],strikethrough:[{inline:"strike",remove:"all"},{inline:"span",styles:{textDecoration:"line-through"},exact:!0}],fontname:{inline:"font",attributes:{face:"%value"}},fontsize:{inline:"font",attributes:{size:function(b){return a.inArray(d,b.value)+1}}},forecolor:{inline:"font",attributes:{color:"%value"}},hilitecolor:{inline:"font",styles:{backgroundColor:"%value"}}}),a.each("b,i,u,strike".split(","),function(a){e.addValidElements(a+"[*]")}),e.getElementRule("font")||e.addValidElements("font[face|size|color|style]"),a.each(c.split(","),function(a){var b=e.getElementRule(a);b&&(b.attributes.align||(b.attributes.align={},b.attributesOrder.push("align")))})}),b.addButton("fontsizeselect",function(){var a=[],c="8pt=1 10pt=2 12pt=3 14pt=4 18pt=5 24pt=6 36pt=7",d=b.settings.fontsize_formats||c;return b.$.each(d.split(" "),function(b,c){var d=c,e=c,f=c.split("=");f.length>1&&(d=f[0],e=f[1]),a.push({text:d,value:e})}),{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:a,fixedWidth:!0,onPostRender:function(){var a=this;b.on("NodeChange",function(){var c;c=b.dom.getParent(b.selection.getNode(),"font"),c?a.value(c.size):a.value("")})},onclick:function(a){a.control.settings.value&&b.execCommand("FontSize",!1,a.control.settings.value)}}}),b.addButton("fontselect",function(){function a(a){a=a.replace(/;$/,"").split(";");for(var b=a.length;b--;)a[b]=a[b].split("=");return a}var c="Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats",e=[],f=a(b.settings.font_formats||c);return d.each(f,function(a,b){e.push({text:{raw:b[0]},value:b[1],textStyle:-1==b[1].indexOf("dings")?"font-family:"+b[1]:""})}),{type:"listbox",text:"Font Family",tooltip:"Font Family",values:e,fixedWidth:!0,onPostRender:function(){var a=this;b.on("NodeChange",function(){var c;c=b.dom.getParent(b.selection.getNode(),"font"),c?a.value(c.face):a.value("")})},onselect:function(a){a.control.settings.value&&b.execCommand("FontName",!1,a.control.settings.value)}}})})}(tinymce); -------------------------------------------------------------------------------- /extends/session_tornadis.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import uuid 3 | import json 4 | import tornadis 5 | import tornado.gen 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Session(dict): 12 | def __init__(self, request_handler): 13 | super(Session, self).__init__() 14 | self.session_id = None 15 | self.session_manager = request_handler.application.session_manager 16 | self.request_handler = request_handler 17 | self.client = None 18 | 19 | @tornado.gen.coroutine 20 | def init_fetch(self): 21 | self.client = yield self.session_manager.get_redis_client() 22 | yield self.fetch_client() 23 | 24 | def get_session_id(self): 25 | if not self.session_id: 26 | self.session_id = self.request_handler.get_secure_cookie(self.session_manager.session_key_name) 27 | return self.session_id 28 | 29 | def generate_session_id(self): 30 | if not self.get_session_id(): 31 | self.session_id = str(uuid.uuid1()) 32 | self.request_handler.set_secure_cookie(self.session_manager.session_key_name, self.session_id, 33 | expires_days=self.session_manager.session_expires_days) 34 | return self.session_id 35 | 36 | @tornado.gen.coroutine 37 | def fetch_client(self): 38 | if self.get_session_id(): 39 | data = yield self.call_client("GET", self.session_id) 40 | if data: 41 | self.update(json.loads(data)) 42 | 43 | @tornado.gen.coroutine 44 | def save(self, expire_time=None): 45 | session_id = self.generate_session_id() 46 | data_json = json.dumps(self) 47 | yield self.call_client("SET", session_id, data_json) 48 | if expire_time: 49 | yield self.call_client("EXPIRE", session_id, expire_time) 50 | 51 | @tornado.gen.coroutine 52 | def call_client(self, *args, **kwargs): 53 | if self.client: 54 | reply = yield self.client.call(*args, **kwargs) 55 | if isinstance(reply, tornadis.TornadisException): 56 | logger.error(reply.message) 57 | else: 58 | raise tornado.gen.Return(reply) 59 | 60 | 61 | class SessionManager(object): 62 | def __init__(self, options): 63 | self.connection_pool = None 64 | self.options = options 65 | self.session_key_name = options['session_key_name'] 66 | self.session_expires_days = options['session_expires_days'] 67 | 68 | def get_connection_pool(self): 69 | if not self.connection_pool: 70 | self.connection_pool = tornadis.ClientPool(host=self.options['host'],port=self.options['port'], 71 | password=self.options['password'], db=self.options['db_no'], 72 | max_size=self.options['max_connections']) 73 | return self.connection_pool 74 | 75 | @tornado.gen.coroutine 76 | def get_redis_client(self): 77 | connection_pool = self.get_connection_pool() 78 | with (yield connection_pool.connected_client()) as client: 79 | if isinstance(client, tornadis.TornadisException): 80 | logger.error(client.message) 81 | else: 82 | raise tornado.gen.Return(client) 83 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from urllib import quote_plus as urlquote 3 | 4 | cookie_keys = dict( 5 | session_key_name="TR_SESSION_ID", 6 | uv_key_name="uv_tag", 7 | ) 8 | 9 | # session相关配置(redis实现) 10 | redis_session_config = dict( 11 | db_no=0, 12 | host="127.0.0.1", 13 | port=6379, 14 | password=None, 15 | max_connections=10, 16 | session_key_name=cookie_keys['session_key_name'], 17 | session_expires_days=7, 18 | ) 19 | 20 | # 站点缓存(redis) 21 | site_cache_config = dict( 22 | db_no=1, 23 | host="127.0.0.1", 24 | port=6379, 25 | password=None, 26 | max_connections=10, 27 | ) 28 | 29 | # 基于redis的消息订阅(发布接收缓存更新消息) 30 | redis_pub_sub_channels = dict( 31 | cache_message_channel="site_cache_message_channel", 32 | ) 33 | 34 | # 消息订阅(基于redis)配置 35 | redis_pub_sub_config = dict( 36 | host="127.0.0.1", 37 | port=6379, 38 | password=None, 39 | autoconnect=True, 40 | channels=[redis_pub_sub_channels['cache_message_channel'],], 41 | ) 42 | 43 | # 数据库配置 44 | database_config = dict( 45 | engine=None, 46 | # engine_url='postgresql+psycopg2://mhq:1qaz2wsx@localhost:5432/blog', 47 | # 如果是使用mysql+mysqldb,在确认所有的库表列都是uft8编码后,依然有字符编码报错, 48 | # 可以尝试在该url末尾加上queryString charset=utf8 49 | engine_url='mysql+mysqlconnector://root:%s@localhost:3306/blog_xtg?charset=utf8' % urlquote('MyPass@123'), 50 | engine_setting=dict( 51 | echo=False, # print sql 52 | echo_pool=False, 53 | # 设置7*60*60秒后回收连接池,默认-1,从不重置 54 | # 该参数会在每个session调用执行sql前校验当前时间与上一次连接时间间隔是否超过pool_recycle,如果超过就会重置。 55 | # 这里设置7小时是为了避免mysql默认会断开超过8小时未活跃过的连接,避免"MySQL server has gone away”错误 56 | # 如果mysql重启或断开过连接,那么依然会在第一次时报"MySQL server has gone away", 57 | # 假如需要非常严格的mysql断线重连策略,可以设置心跳。 58 | # 心跳设置参考https://stackoverflow.com/questions/18054224/python-sqlalchemy-mysql-server-has-gone-away 59 | pool_recycle=25200, 60 | pool_size=20, 61 | max_overflow=20, 62 | ), 63 | ) 64 | 65 | session_keys = dict( 66 | login_user="login_user", 67 | messages="messages", 68 | article_draft="article_draft", 69 | ) 70 | 71 | # 关联model.site_info中的字段 72 | site_cache_keys = dict( 73 | title="title", 74 | signature="signature", 75 | navbar="navbar", 76 | menus="menus", 77 | article_types_not_under_menu="article_types_not_under_menu", 78 | plugins="plugins", 79 | pv="pv", 80 | uv="uv", 81 | article_count="article_count", 82 | comment_count="comment_count", 83 | article_sources="article_sources", 84 | source_articles_count="source_{}_articles_count", 85 | ) 86 | 87 | # 站点相关配置以及tornado的相关参数 88 | config = dict( 89 | debug=False, 90 | log_level="INFO", 91 | log_console=True, 92 | log_file=False, 93 | log_file_path="logs/log", # 末尾自动添加 @端口号.txt_日期 94 | compress_response=True, 95 | xsrf_cookies=True, 96 | cookie_secret="kjsdhfweiofjhewnfiwehfneiwuhniu", 97 | login_url="/auth/login", 98 | port=8888, 99 | max_threads_num=500, 100 | database=database_config, 101 | redis_session=redis_session_config, 102 | session_keys=session_keys, 103 | master=True, # 是否为主从节点中的master节点, 整个集群有且仅有一个,(要提高可用性的话可以用zookeeper来选主,该项目就暂时不做了) 104 | navbar_styles={"inverse": "魅力黑", "default": "优雅白"}, # 导航栏样式 105 | default_avatar_url="identicon", 106 | application=None, # 项目启动后会在这里注册整个server,以便在需要的地方调用,勿修改 107 | ) -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/jquery.tinymce.min.js: -------------------------------------------------------------------------------- 1 | !function(a){function b(){function b(a){"remove"===a&&this.each(function(a,b){var c=e(b);c&&c.remove()}),this.find("span.mceEditor,div.mceEditor").each(function(a,b){var c=tinymce.get(b.id.replace(/_parent$/,""));c&&c.remove()})}function d(a){var c,d=this;if(null!=a)b.call(d),d.each(function(b,c){var d;(d=tinymce.get(c.id))&&d.setContent(a)});else if(d.length>0&&(c=tinymce.get(d[0].id)))return c.getContent()}function e(a){var b=null;return a&&a.id&&g.tinymce&&(b=tinymce.get(a.id)),b}function f(a){return!!(a&&a.length&&g.tinymce&&a.is(":tinymce"))}var h={};a.each(["text","html","val"],function(b,g){var i=h[g]=a.fn[g],j="text"===g;a.fn[g]=function(b){var g=this;if(!f(g))return i.apply(g,arguments);if(b!==c)return d.call(g.filter(":tinymce"),b),i.apply(g.not(":tinymce"),arguments),g;var h="",k=arguments;return(j?g:g.eq(0)).each(function(b,c){var d=e(c);h+=d?j?d.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):d.getContent({save:!0}):i.apply(a(c),k)}),h}}),a.each(["append","prepend"],function(b,d){var g=h[d]=a.fn[d],i="prepend"===d;a.fn[d]=function(a){var b=this;return f(b)?a!==c?("string"==typeof a&&b.filter(":tinymce").each(function(b,c){var d=e(c);d&&d.setContent(i?a+d.getContent():d.getContent()+a)}),g.apply(b.not(":tinymce"),arguments),b):void 0:g.apply(b,arguments)}}),a.each(["remove","replaceWith","replaceAll","empty"],function(c,d){var e=h[d]=a.fn[d];a.fn[d]=function(){return b.call(this,d),e.apply(this,arguments)}}),h.attr=a.fn.attr,a.fn.attr=function(b,g){var i=this,j=arguments;if(!b||"value"!==b||!f(i))return g!==c?h.attr.apply(i,j):h.attr.apply(i,j);if(g!==c)return d.call(i.filter(":tinymce"),g),h.attr.apply(i.not(":tinymce"),j),i;var k=i[0],l=e(k);return l?l.getContent({save:!0}):h.attr.apply(a(k),j)}}var c,d,e,f=[],g=window;a.fn.tinymce=function(c){function h(){var d=[],f=0;e||(b(),e=!0),l.each(function(a,b){var e,g=b.id,h=c.oninit;g||(b.id=g=tinymce.DOM.uniqueId()),tinymce.get(g)||(e=new tinymce.Editor(g,c,tinymce.EditorManager),d.push(e),e.on("init",function(){var a,b=h;l.css("visibility",""),h&&++f==d.length&&("string"==typeof b&&(a=-1===b.indexOf(".")?null:tinymce.resolve(b.replace(/\.\w+$/,"")),b=tinymce.resolve(b)),b.apply(a||tinymce,d))}))}),a.each(d,function(a,b){b.render()})}var i,j,k,l=this,m="";if(!l.length)return l;if(!c)return window.tinymce?tinymce.get(l[0].id):null;if(l.css("visibility","hidden"),g.tinymce||d||!(i=c.script_url))1===d?f.push(h):h();else{d=1,j=i.substring(0,i.lastIndexOf("/")),-1!=i.indexOf(".min")&&(m=".min"),g.tinymce=g.tinyMCEPreInit||{base:j,suffix:m},-1!=i.indexOf("gzip")&&(k=c.language||"en",i=i+(/\?/.test(i)?"&":"?")+"js=true&core=true&suffix="+escape(m)+"&themes="+escape(c.theme||"modern")+"&plugins="+escape(c.plugins||"")+"&languages="+(k||""),g.tinyMCE_GZ||(g.tinyMCE_GZ={start:function(){function b(a){tinymce.ScriptLoader.markDone(tinymce.baseURI.toAbsolute(a))}b("langs/"+k+".js"),b("themes/"+c.theme+"/theme"+m+".js"),b("themes/"+c.theme+"/langs/"+k+".js"),a.each(c.plugins.split(","),function(a,c){c&&(b("plugins/"+c+"/plugin"+m+".js"),b("plugins/"+c+"/langs/"+k+".js"))})},end:function(){}}));var n=document.createElement("script");n.type="text/javascript",n.onload=n.onreadystatechange=function(b){b=b||window.event,2===d||"load"!=b.type&&!/complete|loaded/.test(n.readyState)||(tinymce.dom.Event.domLoaded=1,d=2,c.script_loaded&&c.script_loaded(),h(),a.each(f,function(a,b){b()}))},n.src=i,document.body.appendChild(n)}return l},a.extend(a.expr[":"],{tinymce:function(a){var b;return a.id&&"tinymce"in window&&(b=tinymce.get(a.id),b&&b.editorManager===tinymce)?!0:!1}})}(jQuery); -------------------------------------------------------------------------------- /extends/pub_sub_tornadis.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import tornado.ioloop 3 | import tornado.gen 4 | import tornadis 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class PubSubTornadis(object): 11 | 12 | def __init__(self, redis_pub_sub_config, loop=None): 13 | self.redis_pub_sub_config = redis_pub_sub_config 14 | if not loop: 15 | loop = tornado.ioloop.IOLoop.current() 16 | self.loop = loop 17 | self.autoconnect = self.redis_pub_sub_config['autoconnect'] 18 | self.client = self.get_client() 19 | self.pub_client = None 20 | self.connect_times = 0 21 | self.max_connect_wait_time = 10 22 | 23 | def get_client(self): 24 | client = tornadis.PubSubClient(host=self.redis_pub_sub_config['host'], port=self.redis_pub_sub_config['port'], 25 | password=self.redis_pub_sub_config['password'], 26 | autoconnect=self.autoconnect) 27 | return client 28 | 29 | def get_pub_client(self): 30 | if not self.pub_client: 31 | self.pub_client = tornadis.Client(host=self.redis_pub_sub_config['host'], 32 | port=self.redis_pub_sub_config['port'], 33 | password=self.redis_pub_sub_config['password'], 34 | autoconnect=self.autoconnect) 35 | return self.pub_client 36 | 37 | @tornado.gen.coroutine 38 | def pub_call(self, msg, *channels): 39 | pub_client = self.get_pub_client() 40 | if not pub_client.is_connected(): 41 | yield pub_client.connect() 42 | if not channels: 43 | channels = self.redis_pub_sub_config['channels'] 44 | for channel in channels: 45 | yield pub_client.call("PUBLISH", channel, msg) 46 | 47 | def long_listen(self): 48 | self.loop.add_callback(self.connect_and_listen, self.redis_pub_sub_config['channels']) 49 | 50 | @tornado.gen.coroutine 51 | def connect_and_listen(self, channels): 52 | connected = yield self.client.connect() 53 | if connected: 54 | subscribed = yield self.client.pubsub_subscribe(*channels) 55 | if subscribed: 56 | self.connect_times = 0 57 | yield self.first_do_after_subscribed() 58 | while True: 59 | msgs = yield self.client.pubsub_pop_message() 60 | try: 61 | yield self.do_msg(msgs) 62 | if isinstance(msgs, tornadis.TornadisException): 63 | # closed connection by the server 64 | break 65 | except Exception, e: 66 | logger.exception(e) 67 | self.client.disconnect() 68 | if self.autoconnect: 69 | wait_time = self.connect_times \ 70 | if self.connect_times < self.max_connect_wait_time else self.max_connect_wait_time 71 | logger.warn("等待{}s,重新连接redis消息订阅服务".format(wait_time)) 72 | yield tornado.gen.sleep(wait_time) 73 | self.long_listen() 74 | self.connect_times += 1 75 | 76 | # override 77 | @tornado.gen.coroutine 78 | def first_do_after_subscribed(self): 79 | logger.info("订阅成功") 80 | 81 | # override 82 | @tornado.gen.coroutine 83 | def do_msg(self, msgs): 84 | logger.info("收到订阅消息"+ str(msgs)) 85 | -------------------------------------------------------------------------------- /template/admin/custom_blog_info.html: -------------------------------------------------------------------------------- 1 | {% from model.site_info import SiteCollection %} 2 | {% extends 'admin_base.html' %} 3 | 4 | {% block title2 %} 5 | 基本信息 6 | {% end %} 7 | 8 | {% block admin_content %} 9 |

10 |

基本信息

11 |
12 |
13 |
博客标题:
14 | {{ SiteCollection.title }} 15 |
个性签名:
16 | {{ SiteCollection.signature }} 17 |
导航样式:
18 | 19 | {% if SiteCollection.navbar in navbar_styles %} 20 | {{ navbar_styles[SiteCollection.navbar] }} 21 | {% else %} 22 | {{ navbar_styles['default'] }} 23 | {% end %} 24 | 25 | 32 |
33 |
34 | 35 | 36 | 72 | {% end %} -------------------------------------------------------------------------------- /url_mapping.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import controller.home 3 | import controller.admin 4 | import controller.admin_custom 5 | import controller.admin_article_type 6 | import controller.admin_article 7 | import controller.super 8 | from tornado.web import url 9 | 10 | 11 | # url映射 12 | handlers = [ 13 | url(r"/", controller.home.HomeHandler, name="index"), 14 | url(r"/auth/login", controller.home.LoginHandler, name="login"), 15 | url(r"/auth/logout", controller.home.LogoutHandler, name="logout"), 16 | # articleSource 17 | url(r"/source/([0-9]+)/articles", controller.home.articleSourceHandler, name="articleSource"), 18 | # articleType 19 | url(r"/type/([0-9]+)/articles", controller.home.ArticleTypeHandler, name="articleType"), 20 | # article 21 | url(r"/article/([0-9]+)", controller.home.ArticleHandler, name="article"), 22 | url(r"/article/([0-9]+)/comment", controller.home.ArticleCommentHandler, name="articleComment"), 23 | # admin 24 | url(r"/admin/account", controller.admin.AdminAccountHandler, name="admin.account"), 25 | url(r"/admin/help", controller.admin.AdminHelpHandler, name="admin.help"), 26 | url(r"/admin/account/(change-password|edit-user-info)", 27 | controller.admin.AdminAccountHandler, name="admin.account.update"), 28 | # admin.custom 29 | url(r"/admin/custom/blog-info", 30 | controller.admin_custom.AdminCustomBlogInfoHandler, name="admin.custom.blog_info"), 31 | url(r"/admin/custom/blog-plugin", 32 | controller.admin_custom.AdminCustomBlogPluginHandler, name="admin.custom.blog_plugin"), 33 | url(r"/admin/custom/blog-plugin/(add)", 34 | controller.admin_custom.AdminCustomBlogPluginHandler, name="admin.custom.plugin.action"), 35 | url(r"/admin/custom/blog-plugin/([0-9]+)/(sort-down|sort-up|disable|enable|edit|delete)", 36 | controller.admin_custom.AdminCustomBlogPluginHandler, name="admin.custom.plugin.update"), 37 | # admin.article_type 38 | url(r"/admin/articleType", controller.admin_article_type.AdminArticleTypeHandler, name="admin.articleTypes"), 39 | url(r"/admin/articleType/(add)", 40 | controller.admin_article_type.AdminArticleTypeHandler, name="admin.articleType.action"), 41 | url(r"/admin/articleType/([0-9]+)/(delete|update)", 42 | controller.admin_article_type.AdminArticleTypeHandler, name="admin.articleType.update"), 43 | # admin.article_type_nav (menu) 44 | url(r"/admin/articleType/nav", 45 | controller.admin_article_type.AdminArticleTypeNavHandler, name="admin.articleTypeNavs"), 46 | url(r"/admin/articleType/nav/(add)", 47 | controller.admin_article_type.AdminArticleTypeNavHandler, name="admin.articleTypeNav.action"), 48 | url(r"/admin/articleType/nav/([0-9]+)/(sort-down|sort-up|delete|update)", 49 | controller.admin_article_type.AdminArticleTypeNavHandler, name="admin.articleTypeNav.update"), 50 | # admin.article 51 | url(r"/admin/article/(submit)", controller.admin_article.AdminArticleHandler, name="admin.article.action"), 52 | url(r"/admin/article", controller.admin_article.AdminArticleHandler, name="admin.articles"), 53 | url(r"/admin/article/([0-9]+)", controller.admin_article.AdminArticleHandler, name="admin.article"), 54 | url(r"/admin/article/([0-9]+)/(delete)", controller.admin_article.AdminArticleHandler, name="admin.article.update"), 55 | 56 | url(r"/admin/comment", controller.admin_article.AdminArticleCommentHandler, name="admin.comments"), 57 | url(r"/admin/article/([0-9]+)/comment/([0-9]+)/(disable|enable|delete)", 58 | controller.admin_article.AdminArticleCommentHandler, name="admin.comment.update"), 59 | # super.init 60 | url(r"/super/init", controller.super.SuperHandler, name="super.init"), 61 | ] -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/textcolor/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("textcolor",function(a){function b(b){var c;return a.dom.getParents(a.selection.getStart(),function(a){var d;(d=a.style["forecolor"==b?"color":"background-color"])&&(c=d)}),c}function c(){var b,c,d=[];for(c=a.settings.textcolor_map||["000000","Black","993300","Burnt orange","333300","Dark olive","003300","Dark green","003366","Dark azure","000080","Navy Blue","333399","Indigo","333333","Very dark gray","800000","Maroon","FF6600","Orange","808000","Olive","008000","Green","008080","Teal","0000FF","Blue","666699","Grayish blue","808080","Gray","FF0000","Red","FF9900","Amber","99CC00","Yellow green","339966","Sea green","33CCCC","Turquoise","3366FF","Royal blue","800080","Purple","999999","Medium gray","FF00FF","Magenta","FFCC00","Gold","FFFF00","Yellow","00FF00","Lime","00FFFF","Aqua","00CCFF","Sky blue","993366","Red violet","FFFFFF","White","FF99CC","Pink","FFCC99","Peach","FFFF99","Light yellow","CCFFCC","Pale green","CCFFFF","Pale cyan","99CCFF","Light sky blue","CC99FF","Plum"],b=0;b
'+(c?"×":"")+"
"}var d,e,f,g,h,k,l,m=this,n=m._id,o=0;for(d=c(),d.push({text:tinymce.translate("No color"),color:"transparent"}),f='',g=d.length-1,k=0;j>k;k++){for(f+="",h=0;i>h;h++)l=k*i+h,l>g?f+="":(e=d[l],f+=b(e.color,e.text));f+=""}if(a.settings.color_picker_callback){for(f+='",f+="",h=0;i>h;h++)f+=b("","Custom color");f+=""}return f+="
"}function e(b,c){a.undoManager.transact(function(){a.focus(),a.formatter.apply(b,{value:c}),a.nodeChanged()})}function f(b){a.undoManager.transact(function(){a.focus(),a.formatter.remove(b,{value:null},null,!0),a.nodeChanged()})}function g(c){function d(a){k.hidePanel(),k.color(a),e(k.settings.format,a)}function g(){k.hidePanel(),k.resetColor(),f(k.settings.format)}function h(a,b){a.style.background=b,a.setAttribute("data-mce-color",b)}var j,k=this.parent();tinymce.DOM.getParent(c.target,".mce-custom-color-btn")&&(k.hidePanel(),a.settings.color_picker_callback.call(a,function(a){var b,c,e,f=k.panel.getEl().getElementsByTagName("table")[0];for(b=tinymce.map(f.rows[f.rows.length-1].childNodes,function(a){return a.firstChild}),e=0;ee;e++)h(b[e],b[e+1].getAttribute("data-mce-color"));h(c,a),d(a)},b(k.settings.format))),j=c.target.getAttribute("data-mce-color"),j?(this.lastId&&document.getElementById(this.lastId).setAttribute("aria-selected",!1),c.target.setAttribute("aria-selected",!0),this.lastId=c.target.id,"transparent"==j?g():d(j)):null!==j&&k.hidePanel()}function h(){var a=this;a._color?e(a.settings.format,a._color):f(a.settings.format)}var i,j;j=a.settings.textcolor_rows||5,i=a.settings.textcolor_cols||8,a.addButton("forecolor",{type:"colorbutton",tooltip:"Text color",format:"forecolor",panel:{role:"application",ariaRemember:!0,html:d,onclick:g},onclick:h}),a.addButton("backcolor",{type:"colorbutton",tooltip:"Background color",format:"hilitecolor",panel:{role:"application",ariaRemember:!0,html:d,onclick:g},onclick:h})}); -------------------------------------------------------------------------------- /static/css/prism.css: -------------------------------------------------------------------------------- 1 | /* 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 */ 2 | /** 3 | * prism.js default theme for JavaScript, CSS and HTML 4 | * Based on dabblet (http://dabblet.com) 5 | * @author Lea Verou 6 | */ 7 | 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: black; 11 | background: none; 12 | text-shadow: 0 1px white; 13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 14 | direction: ltr; 15 | text-align: left; 16 | white-space: pre; 17 | word-spacing: normal; 18 | word-break: normal; 19 | word-wrap: normal; 20 | line-height: 1.5; 21 | 22 | -moz-tab-size: 4; 23 | -o-tab-size: 4; 24 | tab-size: 4; 25 | 26 | -webkit-hyphens: none; 27 | -moz-hyphens: none; 28 | -ms-hyphens: none; 29 | hyphens: none; 30 | } 31 | 32 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 33 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 34 | text-shadow: none; 35 | background: #b3d4fc; 36 | } 37 | 38 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 39 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 40 | text-shadow: none; 41 | background: #b3d4fc; 42 | } 43 | 44 | @media print { 45 | code[class*="language-"], 46 | pre[class*="language-"] { 47 | text-shadow: none; 48 | } 49 | } 50 | 51 | /* Code blocks */ 52 | pre[class*="language-"] { 53 | padding: 1em; 54 | margin: .5em 0; 55 | overflow: auto; 56 | } 57 | 58 | :not(pre) > code[class*="language-"], 59 | pre[class*="language-"] { 60 | background: #f5f2f0; 61 | } 62 | 63 | /* Inline code */ 64 | :not(pre) > code[class*="language-"] { 65 | padding: .1em; 66 | border-radius: .3em; 67 | white-space: normal; 68 | } 69 | 70 | .token.comment, 71 | .token.prolog, 72 | .token.doctype, 73 | .token.cdata { 74 | color: slategray; 75 | } 76 | 77 | .token.punctuation { 78 | color: #999; 79 | } 80 | 81 | .namespace { 82 | opacity: .7; 83 | } 84 | 85 | .token.property, 86 | .token.tag, 87 | .token.boolean, 88 | .token.number, 89 | .token.constant, 90 | .token.symbol, 91 | .token.deleted { 92 | color: #905; 93 | } 94 | 95 | .token.selector, 96 | .token.attr-name, 97 | .token.string, 98 | .token.char, 99 | .token.builtin, 100 | .token.inserted { 101 | color: #690; 102 | } 103 | 104 | .token.operator, 105 | .token.entity, 106 | .token.url, 107 | .language-css .token.string, 108 | .style .token.string { 109 | color: #a67f59; 110 | background: hsla(0, 0%, 100%, .5); 111 | } 112 | 113 | .token.atrule, 114 | .token.attr-value, 115 | .token.keyword { 116 | color: #07a; 117 | } 118 | 119 | .token.function { 120 | color: #DD4A68; 121 | } 122 | 123 | .token.regex, 124 | .token.important, 125 | .token.variable { 126 | color: #e90; 127 | } 128 | 129 | .token.important, 130 | .token.bold { 131 | font-weight: bold; 132 | } 133 | .token.italic { 134 | font-style: italic; 135 | } 136 | 137 | .token.entity { 138 | cursor: help; 139 | } 140 | 141 | -------------------------------------------------------------------------------- /service/menu_service.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | 4 | from sqlalchemy import func 5 | 6 | from article_type_service import ArticleTypeService 7 | from model.models import Menu 8 | from model.search_params.menu_params import MenuSearchParams 9 | from . import BaseService 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class MenuService(object): 15 | @staticmethod 16 | def page_menus(db_session, pager, search_params): 17 | query = db_session.query(Menu) 18 | if search_params: 19 | if search_params.order_mode == MenuSearchParams.ORDER_MODE_ORDER_ASC: 20 | query = query.order_by(Menu.order.asc()) 21 | pager = BaseService.query_pager(query, pager) 22 | if pager.result: 23 | for menu in pager.result: 24 | menu.fetch_all_types() 25 | return pager 26 | 27 | @staticmethod 28 | def add_menu(db_session, menu): 29 | try: 30 | menu_to_save = Menu(**menu) 31 | menu_to_save.order = MenuService.get_max_order(db_session) + 1 32 | db_session.add(menu_to_save) 33 | db_session.commit() 34 | return menu_to_save 35 | except Exception, e: 36 | logger.exception(e) 37 | return None 38 | 39 | @staticmethod 40 | def get_max_order(db_session): 41 | max_order = db_session.query(func.max(Menu.order)).scalar() 42 | if max_order is None: 43 | max_order = 0 44 | return max_order 45 | 46 | @staticmethod 47 | def list_menus(db_session, show_types=False): 48 | menus = db_session.query(Menu).order_by(Menu.order.asc()).all() 49 | if not menus: 50 | menus = [] 51 | else: 52 | if show_types: 53 | for menu in menus: 54 | menu.fetch_all_types(only_show_not_hide=True) 55 | return menus 56 | 57 | @staticmethod 58 | def sort_up(db_session, menu_id): 59 | menu = db_session.query(Menu).get(menu_id) 60 | if menu: 61 | menu_up = db_session.query(Menu). \ 62 | filter(Menu.order < menu.order).order_by(Menu.order.desc()).first() 63 | if menu_up: 64 | order_tmp = menu.order 65 | menu.order = menu_up.order 66 | menu_up.order = order_tmp 67 | db_session.commit() 68 | return True 69 | return False 70 | 71 | @staticmethod 72 | def sort_down(db_session, menu_id): 73 | menu = db_session.query(Menu).get(menu_id) 74 | if menu: 75 | menu_up = db_session.query(Menu). \ 76 | filter(Menu.order > menu.order).order_by(Menu.order.asc()).first() 77 | if menu_up: 78 | order_tmp = menu.order 79 | menu.order = menu_up.order 80 | menu_up.order = order_tmp 81 | db_session.commit() 82 | return True 83 | return False 84 | 85 | @staticmethod 86 | def update(db_session, menu_id, menu_to_update): 87 | count = 0 88 | if menu_to_update: 89 | if "id" in menu_to_update: 90 | menu_to_update.remove("id") 91 | count = db_session.query(Menu).filter(Menu.id == menu_id).update(menu_to_update) 92 | if count: 93 | db_session.commit() 94 | return count 95 | 96 | @staticmethod 97 | def delete(db_session, menu_id): 98 | ArticleTypeService.set_article_type_menu_id_none(db_session, menu_id, auto_commit=False) 99 | count = db_session.query(Menu).filter(Menu.id == menu_id).delete() 100 | if count: 101 | db_session.commit() 102 | return count 103 | -------------------------------------------------------------------------------- /service/comment_service.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | 4 | from model.models import Comment 5 | from sqlalchemy.sql import func 6 | from sqlalchemy.orm import joinedload 7 | from model.search_params.comment_params import CommentSearchParams 8 | from . import BaseService 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class CommentService(object): 14 | @staticmethod 15 | def get_comment(db_session, comment_id): 16 | return db_session.query(Comment).get(comment_id) 17 | 18 | @staticmethod 19 | def get_max_floor(db_session, article_id): 20 | max_floor = db_session.query(func.max(Comment.floor)).filter(Comment.article_id == article_id).scalar() 21 | return max_floor if max_floor else 0; 22 | 23 | @staticmethod 24 | def add_comment(db_session, article_id, comment): 25 | max_floor = CommentService.get_max_floor(db_session, article_id) 26 | floor = max_floor + 1 27 | comment_to_add = Comment(content=comment['content'], author_name=comment['author_name'], 28 | author_email=comment['author_email'], article_id=article_id, 29 | comment_type=comment['comment_type'], rank=comment['rank'], floor=floor, 30 | reply_to_id=comment['reply_to_id'], reply_to_floor=comment['reply_to_floor']) 31 | db_session.add(comment_to_add) 32 | db_session.commit() 33 | return comment_to_add 34 | 35 | @staticmethod 36 | def update_comment_disabled(db_session, article_id, comment_id, disabled): 37 | updated = db_session.query(Comment).filter(Comment.article_id == article_id, Comment.id == comment_id).\ 38 | update({Comment.disabled: disabled}) 39 | db_session.commit() 40 | return updated 41 | 42 | @staticmethod 43 | def delete_comment(db_session, article_id, comment_id): 44 | comment = CommentService.get_comment(db_session, comment_id); 45 | if comment and comment.article_id == int(article_id): 46 | db_session.delete(comment) 47 | db_session.commit() 48 | return comment 49 | return None 50 | 51 | @staticmethod 52 | def page_comments(db_session, pager, params): 53 | query = db_session.query(Comment) 54 | if params: 55 | if params.article_id: 56 | query = query.filter(Comment.article_id == params.article_id) 57 | if params.show_article_id_title: 58 | query = query.options(joinedload(Comment.article).load_only("id", "title")) 59 | if params.order_mode == CommentSearchParams.ORDER_MODE_CREATE_TIME_ASC: 60 | query = query.order_by(Comment.create_time.asc()) 61 | elif params.order_mode == CommentSearchParams.ORDER_MODE_CREATE_TIME_DESC: 62 | query = query.order_by(Comment.create_time.desc()) 63 | pager = BaseService.query_pager(query, pager) 64 | return pager 65 | 66 | @staticmethod 67 | def remove_by_article_id(db_session, article_id, commit=True): 68 | try: 69 | comments = db_session.query(Comment).filter(Comment.article_id == article_id).all() 70 | db_session.query(Comment).filter(Comment.article_id == article_id).delete() 71 | if commit: 72 | db_session.commit() 73 | return comments 74 | except Exception, e: 75 | logger.exception(e) 76 | return None 77 | 78 | @staticmethod 79 | def get_comment_count(db_session): 80 | comment_count = db_session.query(Comment).count() 81 | return comment_count 82 | 83 | @staticmethod 84 | def get_comments_count_subquery(db_session): 85 | stmt = db_session.query(Comment.article_id, func.count('*').label('comments_count')). \ 86 | group_by(Comment.article_id).subquery() 87 | return stmt -------------------------------------------------------------------------------- /template/article_detials.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | {{ article.title }} 5 | {% end %} 6 | {% block private_stylesheet %} 7 | 8 | {% end %} 9 | {% block content %} 10 |
11 |
12 |

13 | {{ article.title }} 14 |

15 |
16 | 35 | 36 |
37 |
38 | 39 | 42 |
43 |

44 |
45 |

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

51 |
52 | {% if current_user %} 53 |
54 | 55 | 59 | 60 |
61 | {% end %} 62 |
63 | 64 |

评论

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

发表评论

69 |
70 |
71 |
72 | {% module xsrf_form_html() %} 73 | 74 | 76 | 77 | 79 | 80 | 81 | 84 |
85 |
86 |
87 | 88 |
89 | {% end %} 90 | 91 | {% block script %} 92 | 93 | 94 | 95 | {% end %} 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [blog_xtg](https://github.com/xtg20121013/blog_xtg)是我个人写的一个开源分布式博客,其web框架使用的是tornado(一个基于异步IO的python web框架)。同时我把它设计成一个可以多进程多主机部署的分布式架构,如果你对异步IO的web框架感兴趣,或者对高并发分布式的架构感兴趣并处于入门阶段,那么很希望你来尝试blog_xtg,一定会有所收获。 2 | 3 | ### 一、为什么写blog_xtg 4 | 作为一个码农怎么能没有一个属于自己的个人博客呢?即便没人看,作为日记来记录编码生涯也是很有必要。其实开源的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的分布式架构博客。 5 | 6 | ### 二、blog_xtg简介 7 | 首先非常感谢开源博客[Blog_mini](https://github.com/xpleaf/Blog_mini),因为整个blog_xtg是基于[Blog_mini](https://github.com/xpleaf/Blog_mini)重构的。 8 | 9 | 我不太擅长前端,所以基本照搬[Blog_mini](https://github.com/xpleaf/Blog_mini)的页面,但是整个后端逻辑都是重写的,以下是与[Blog_mini](https://github.com/xpleaf/Blog_mini)的主要区别: 10 | 11 | 1. 改用tornado框架,是个基于异步IO的web server。 12 | 2. 分布式架构,可以多进程多主机启动server实例,再通过nginx等代理服务器做负载均衡,实现横向扩展提高并发性能。 13 | 3. 提高多数主要页面访问性能。对频繁查询的组件(例如博客标题、菜单、公告、访问统计)进行缓存,优化sql查询(多条sql语句合并一次执行、仅查需要的字段,例如搜索博文列表不查博文的具体内容)以提高首页博文等主要页面访问性能。 14 | 4. 访问统计改为日pv和日uv。 15 | 5. 博文编辑器改为markdown编辑器。 16 | 6. 引入alembic管理数据库版本。 17 | 7. 可使用docker快速部署。 18 | 19 | 但是,作为一个个人blog,其实并不需要分布式的架构,即便引入了这样的架构,我依然希望其他开发者能够快捷的搭建环境并上手使用,因此blog_xtg只是简单的实现了分布式,并不能保证绝对的高可用,主从需要启动实例时手动指定,存在单点故障的可能,如果有开发者希望以此架构扩展到大型生产环境请自行配合zookeeper等实现动态选主+完整的日志分析、性能监控以及完善报警机制来保证高可用。 20 | 21 | **注:** blog_xtg目前架构并不需要考虑线程安全问题,因为tornado是单线程的,仅用到多线程的地方只有通过线程池访问数据库,数据库连接session是线程局部变量,其他并无线程间共享的变量,不会带来线程安全问题。 22 | 23 | ### 三、blog_xtg部署与开发环境搭建 24 | #### 1. 如果你熟悉docker,那么可以用docker来快速部署。 25 | 26 | #新建数据库(理论上支持sqlalchemy支持的所有数据库,表会自动创建更新) 27 | #搭建redis 28 | #下载config.py并编辑相关配置(修改数据库、redis、日志等) 29 | curl -o xxx/config.py https://raw.githubusercontent.com/xtg20121013/blog_xtg/master/config.py 30 | #通过docker启动后即可访问 31 | 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 32 | 这个镜像启动时包含两个server实例(一主一从)+nginx(动静分离、负载均衡)+supervisor(进程管理),当然你也可以根据自己的需求构建镜像,Dockerfile在项目/docker目录下。 33 | #### 2. 构建运行环境 34 | ###### 需要安装以下组件: 35 | 36 | 1. python2.7(python3 没试过,不知道行不行) 37 | 2. mysql(或者其他sqlalchemy支持的数据库) 38 | 3. redis 39 | 40 | ###### clone项目,安装依赖: 41 | 42 | git clone https://github.com/xtg20121013/blog_xtg.git 43 | #项目依赖(如果用的不是mysql可以将MySQL-python替换使用的数据库成所对应的依赖包) 44 | pip install -r requirements.txt 45 | ###### 创建数据库(注意使用utf-8编码) 46 | ###### 启动redis 47 | ###### 修改config.py,配置数据库、redis、日志等 48 | ###### 创建数据库或更新表 49 | python main.py upgradedb 50 | ###### 启动server 51 | python main.py --master=true --port=8888 52 | 53 | ###### 初始化管理员账户 54 | 访问http://[host]:[port]/super/init注册管理员账号。 55 | 56 | 注:仅没有任何管理员时才可以访问到该页面。 57 | 58 | ### 四、开发注意事项 59 | #### 1.blog_xtg是个异步IO的架构,相对于常见的同步IO框架,需要注意以下几点: 60 | 61 | - IO密集型的操作请务必使用异步的client,否则无法利用到异步的优势 62 | - 由于多数异步IO的框架都是单线程的,所以对于CPU密集型的操作最好交由外部系统处理,防止阻塞,大型项目可以配合消息队列使用更佳 63 | - 如果必须用同步的IO组件,可以配合线程池使用(blog_xtg中使用了sqlalchemy就是配合线程池使用的) 64 | - 如果你是ORM+线程池使用(blog_xtg中就是sqlalchemy+线程池),一般的ORM都有lazy load的机制,在异步框架中请勿使用,因为lazy load的执行在主线程中,很可能会阻塞主线程,影响别的请求。 65 | 66 | #### 2.blog_xtg是分布式的架构,相对于单进程的项目一般需要注意以下几点: 67 | 68 | - 多实例间的日志冲突。 69 | - 多实例间的缓存同步。 70 | - 多实例间的session同步。 71 | - 多实例间主从关系,例如一些定时任务可能主需要集群中一个节点处理。 72 | 73 | 当然以上几点都可以从blog_xtg的源代码中找到至少一种解决方案。 74 | 75 | 如果你对异步IO的web框架、分布式的架构感兴趣,或者想对blog_xtg做二次开发,那么你可以阅读以下blog_xtg的其他相关博文,并配合源代码学习,一定会很快掌握。 76 | 77 | 1. [开源博客blog_xtg技术架构-非阻塞IO web框架tornado](http://blog.52xtg.com/article/10) 78 | 79 | 80 | #### 3.对于博文编辑的markdown的问题: 81 | 82 | 我用的是[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) 83 | 84 | ### 五、技术支持 85 | 如果你有任何疑问,可以给我留言: 86 | 87 | 附: 88 | 89 | - 个人博客:[http://blog.52xtg.com](http://blog.52xtg.com) 90 | 91 | - 简书博客:[http://www.jianshu.com/u/dfb6bf87c35e](http://www.jianshu.com/u/dfb6bf87c35e) 92 | 93 | - 试用博客:[http://blogdemo.52xtg.com](http://blogdemo.52xtg.com) 94 | 95 | - blog_xtg的github地址:[https://github.com/xtg20121013/blog_xtg](https://github.com/xtg20121013/blog_xtg) 96 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/template/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("template",function(a){function b(b){return function(){var c=a.settings.templates;return"function"==typeof c?void c(b):void("string"==typeof c?tinymce.util.XHR.send({url:c,success:function(a){b(tinymce.util.JSON.parse(a))}}):b(c))}}function c(b){function c(b){function c(b){if(-1==b.indexOf("")){var c="";tinymce.each(a.contentCSS,function(b){c+=''}),b=""+c+""+b+""}b=f(b,"template_preview_replace_values");var e=d.find("iframe")[0].getEl().contentWindow.document;e.open(),e.write(b),e.close()}var g=b.control.value();g.url?tinymce.util.XHR.send({url:g.url,success:function(a){e=a,c(e)}}):(e=g.content,c(e)),d.find("#description")[0].text(b.control.value().description)}var d,e,h=[];if(!b||0===b.length){var i=a.translate("No templates defined.");return void a.notificationManager.open({text:i,type:"info"})}tinymce.each(b,function(a){h.push({selected:!h.length,text:a.title,value:{url:a.url,content:a.content,description:a.description}})}),d=a.windowManager.open({title:"Insert template",layout:"flex",direction:"column",align:"stretch",padding:15,spacing:10,items:[{type:"form",flex:0,padding:0,items:[{type:"container",label:"Templates",items:{type:"listbox",label:"Templates",name:"template",values:h,onselect:c}}]},{type:"label",name:"description",label:"Description",text:"\xa0"},{type:"iframe",flex:1,border:1}],onsubmit:function(){g(!1,e)},width:a.getParam("template_popup_width",600),height:a.getParam("template_popup_height",500)}),d.find("listbox")[0].fire("select")}function d(b,c){function d(a,b){if(a=""+a,a.length0&&(i=k.create("div",null),i.appendChild(j[0].cloneNode(!0))),h(k.select("*",i),function(b){g(b,a.getParam("template_cdate_classes","cdate").replace(/\s+/g,"|"))&&(b.innerHTML=d(a.getParam("template_cdate_format",a.getLang("template.cdate_format")))),g(b,a.getParam("template_mdate_classes","mdate").replace(/\s+/g,"|"))&&(b.innerHTML=d(a.getParam("template_mdate_format",a.getLang("template.mdate_format")))),g(b,a.getParam("template_selected_content_classes","selcontent").replace(/\s+/g,"|"))&&(b.innerHTML=l)}),e(i),a.execCommand("mceInsertContent",!1,i.innerHTML),a.addVisual()}var h=tinymce.each;a.addCommand("mceInsertTemplate",g),a.addButton("template",{title:"Insert template",onclick:b(c)}),a.addMenuItem("template",{text:"Insert template",onclick:b(c),context:"insert"}),a.on("PreProcess",function(b){var c=a.dom;h(c.select("div",b.node),function(b){c.hasClass(b,"mceTmpl")&&(h(c.select("*",b),function(b){c.hasClass(b,a.getParam("template_mdate_classes","mdate").replace(/\s+/g,"|"))&&(b.innerHTML=d(a.getParam("template_mdate_format",a.getLang("template.mdate_format"))))}),e(b))})})}); -------------------------------------------------------------------------------- /service/plugin_service.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | from sqlalchemy import func 4 | from model.models import Plugin 5 | from model.search_params.plugin_params import PluginSearchParams 6 | from . import BaseService 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class PluginService(object): 12 | 13 | @staticmethod 14 | def get(db_session, plugin_id): 15 | plugin = db_session.query(Plugin).get(plugin_id) 16 | return plugin 17 | 18 | @staticmethod 19 | def get_editable(db_session, plugin_id): 20 | plugin = db_session.query(Plugin).get(plugin_id) 21 | if plugin: 22 | plugin = plugin if plugin.content != 'system_plugin' else None 23 | return plugin 24 | 25 | @staticmethod 26 | def list_plugins(db_session): 27 | plugins = db_session.query(Plugin).order_by(Plugin.order.asc()).all() 28 | return plugins 29 | 30 | @staticmethod 31 | def page_plugins(db_session, pager, search_params): 32 | query = db_session.query(Plugin) 33 | if search_params: 34 | if search_params.order_mode == PluginSearchParams.ORDER_MODE_ORDER_ASC: 35 | query = query.order_by(Plugin.order.asc()) 36 | pager = BaseService.query_pager(query, pager) 37 | return pager 38 | 39 | @staticmethod 40 | def save(db_session, plugin): 41 | try: 42 | plugin_to_save = Plugin(**plugin) 43 | plugin_to_save.order = PluginService.get_max_order(db_session) + 1 44 | db_session.add(plugin_to_save) 45 | db_session.commit() 46 | return plugin_to_save 47 | except Exception, e: 48 | logger.exception(e) 49 | return None 50 | 51 | @staticmethod 52 | def get_max_order(db_session): 53 | max_order = db_session.query(func.max(Plugin.order)).scalar() 54 | if max_order is None: 55 | max_order = 0 56 | return max_order 57 | 58 | @staticmethod 59 | def sort_up(db_session, plugin_id): 60 | plugin = db_session.query(Plugin).get(plugin_id) 61 | if plugin: 62 | plugin_up = db_session.query(Plugin).\ 63 | filter(Plugin.order < plugin.order).order_by(Plugin.order.desc()).first() 64 | if plugin_up: 65 | order_tmp = plugin.order 66 | plugin.order = plugin_up.order 67 | plugin_up.order = order_tmp 68 | db_session.commit() 69 | return True 70 | return False 71 | 72 | @staticmethod 73 | def sort_down(db_session, plugin_id): 74 | plugin = db_session.query(Plugin).get(plugin_id) 75 | if plugin: 76 | plugin_up = db_session.query(Plugin).\ 77 | filter(Plugin.order > plugin.order).order_by(Plugin.order.asc()).first() 78 | if plugin_up: 79 | order_tmp = plugin.order 80 | plugin.order = plugin_up.order 81 | plugin_up.order = order_tmp 82 | db_session.commit() 83 | return True 84 | return False 85 | 86 | @staticmethod 87 | def update_disabled(db_session, plugin_id, disabled): 88 | update_count = db_session.query(Plugin).filter(Plugin.id == plugin_id).update({Plugin.disabled:disabled}) 89 | if update_count: 90 | db_session.commit() 91 | return update_count 92 | 93 | @staticmethod 94 | def delete(db_session, plugin_id): 95 | plugin = PluginService.get_editable(db_session, plugin_id) 96 | if plugin: 97 | db_session.delete(plugin) 98 | db_session.commit() 99 | return True 100 | return False 101 | 102 | @staticmethod 103 | def update(db_session, plugin_id, plugin_to_update): 104 | plugin = PluginService.get_editable(db_session, plugin_id) 105 | if plugin: 106 | plugin.title = plugin_to_update['title'] 107 | plugin.note = plugin_to_update['note'] 108 | plugin.content = plugin_to_update['content'] 109 | db_session.commit() 110 | return True 111 | return False 112 | 113 | -------------------------------------------------------------------------------- /service/article_type_service.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | from sqlalchemy.orm import contains_eager, joinedload 4 | from model.models import ArticleType, ArticleTypeSetting 5 | from model.search_params.article_type_params import ArticleTypeSearchParams 6 | from . import BaseService 7 | from article_service import ArticleService 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class ArticleTypeService(object): 13 | @staticmethod 14 | def page_article_types(db_session, pager, search_params): 15 | query = db_session.query(ArticleType) 16 | if search_params: 17 | if search_params.order_mode == ArticleTypeSearchParams.ORDER_MODE_ID_DESC: 18 | query = query.order_by(ArticleType.id.desc()) 19 | if search_params.show_setting: 20 | query = query.options(joinedload(ArticleType.setting)) 21 | pager = BaseService.query_pager(query, pager) 22 | if pager.result: 23 | if search_params.show_articles_count: 24 | for article_type in pager.result: 25 | article_type.fetch_articles_count() 26 | return pager 27 | 28 | @staticmethod 29 | def list_article_types_not_under_menu(db_session): 30 | article_types_not_under_menu = db_session.query(ArticleType).join(ArticleType.setting).\ 31 | filter(ArticleType.menu_id.is_(None), ArticleTypeSetting.hide.isnot(True)).\ 32 | options(contains_eager(ArticleType.setting)).all() 33 | return article_types_not_under_menu 34 | 35 | @staticmethod 36 | def add_article_type(db_session, article_type): 37 | try: 38 | article_type_to_add = ArticleType(name=article_type["name"], introduction=article_type["introduction"], 39 | menu_id=article_type["menu_id"], 40 | setting=ArticleTypeSetting(name=article_type["name"], 41 | hide=article_type["setting_hide"],),) 42 | db_session.add(article_type_to_add) 43 | db_session.commit() 44 | return article_type_to_add 45 | except Exception, e: 46 | logger.exception(e) 47 | return None 48 | 49 | @staticmethod 50 | def update_article_type(db_session, article_type_id, article_type): 51 | try: 52 | article_type_to_update=db_session.query(ArticleType).get(article_type_id) 53 | if article_type_to_update and not article_type_to_update.is_protected: 54 | article_type_to_update.name=article_type['name'] 55 | article_type_to_update.introduction = article_type['introduction'] 56 | article_type_to_update.menu_id = article_type['menu_id'] 57 | if not article_type_to_update.setting: 58 | article_type_to_update.setting = ArticleTypeSetting(name=article_type["name"], 59 | hide=article_type["setting_hide"],) 60 | else: 61 | article_type_to_update.setting.hide = article_type['setting_hide'] 62 | db_session.commit() 63 | return True 64 | except Exception, e: 65 | logger.exception(e) 66 | return False 67 | 68 | @staticmethod 69 | def delete(db_session, article_type_id): 70 | article_type_to_delete = db_session.query(ArticleType).get(article_type_id) 71 | if article_type_to_delete and not article_type_to_delete.is_protected: 72 | # 未将文章分类移除到未分类 73 | ArticleService.set_article_type_default_by_article_type_id(db_session, article_type_id, False) 74 | db_session.delete(article_type_to_delete.setting) 75 | db_session.delete(article_type_to_delete) 76 | db_session.commit() 77 | return 1 78 | return 0 79 | 80 | @staticmethod 81 | def set_article_type_menu_id_none(db_session, menu_id, auto_commit=True): 82 | db_session.query(ArticleType).filter(ArticleType.menu_id == menu_id).update({"menu_id": None}) 83 | if auto_commit: 84 | db_session.commit() 85 | 86 | @staticmethod 87 | def list_simple(db_session): 88 | article_types = db_session.query(ArticleType.id, ArticleType.name).all() 89 | return article_types 90 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/link/plugin.min.js: -------------------------------------------------------------------------------- 1 | tinymce.PluginManager.add("link",function(a){function b(b){return function(){var c=a.settings.link_list;"string"==typeof c?tinymce.util.XHR.send({url:c,success:function(a){b(tinymce.util.JSON.parse(a))}}):"function"==typeof c?c(b):b(c)}}function c(a,b,c){function d(a,c){return c=c||[],tinymce.each(a,function(a){var e={text:a.text||a.title};a.menu?e.menu=d(a.menu):(e.value=a.value,b&&b(e)),c.push(e)}),c}return d(a,c||[])}function d(b){function d(a){var b=l.find("#text");(!b.value()||a.lastControl&&b.value()==a.lastControl.text())&&b.value(a.control.text()),l.find("#href").value(a.control.value())}function e(b){var c=[];return tinymce.each(a.dom.select("a:not([href])"),function(a){var d=a.name||a.id;d&&c.push({text:d,value:"#"+d,selected:-1!=b.indexOf("#"+d)})}),c.length?(c.unshift({text:"None",value:""}),{name:"anchor",type:"listbox",label:"Anchors",values:c,onselect:d}):void 0}function f(){!k&&0===u.text.length&&m&&this.parent().parent().find("#text")[0].value(this.value())}function g(b){var c=b.meta||{};o&&o.value(a.convertURL(this.value(),"href")),tinymce.each(b.meta,function(a,b){l.find("#"+b).value(a)}),c.text||f.call(this)}function h(a){var b=v.getContent();if(/]+>[^<]+<\/a>$/.test(b)||-1==b.indexOf("href=")))return!1;if(a){var c,d=a.childNodes;if(0===d.length)return!1;for(c=d.length-1;c>=0;c--)if(3!=d[c].nodeType)return!1}return!0}var i,j,k,l,m,n,o,p,q,r,s,t,u={},v=a.selection,w=a.dom;i=v.getNode(),j=w.getParent(i,"a[href]"),m=h(),u.text=k=j?j.innerText||j.textContent:v.getContent({format:"text"}),u.href=j?w.getAttrib(j,"href"):"",j?u.target=w.getAttrib(j,"target"):a.settings.default_link_target&&(u.target=a.settings.default_link_target),(t=w.getAttrib(j,"rel"))&&(u.rel=t),(t=w.getAttrib(j,"class"))&&(u["class"]=t),(t=w.getAttrib(j,"title"))&&(u.title=t),m&&(n={name:"text",type:"textbox",size:40,label:"Text to display",onchange:function(){u.text=this.value()}}),b&&(o={type:"listbox",label:"Link list",values:c(b,function(b){b.value=a.convertURL(b.value||b.url,"href")},[{text:"None",value:""}]),onselect:d,value:a.convertURL(u.href,"href"),onPostRender:function(){o=this}}),a.settings.target_list!==!1&&(a.settings.target_list||(a.settings.target_list=[{text:"None",value:""},{text:"New window",value:"_blank"}]),q={name:"target",type:"listbox",label:"Target",values:c(a.settings.target_list)}),a.settings.rel_list&&(p={name:"rel",type:"listbox",label:"Rel",values:c(a.settings.rel_list)}),a.settings.link_class_list&&(r={name:"class",type:"listbox",label:"Class",values:c(a.settings.link_class_list,function(b){b.value&&(b.textStyle=function(){return a.formatter.getCssText({inline:"a",classes:[b.value]})})})}),a.settings.link_title!==!1&&(s={name:"title",type:"textbox",label:"Title",value:u.title}),l=a.windowManager.open({title:"Insert link",data:u,body:[{name:"href",type:"filepicker",filetype:"file",size:40,autofocus:!0,label:"Url",onchange:g,onkeyup:f},n,s,e(u.href),o,p,q,r],onSubmit:function(b){function c(b,c){var d=a.selection.getRng();tinymce.util.Delay.setEditorTimeout(a,function(){a.windowManager.confirm(b,function(b){a.selection.setRng(d),c(b)})})}function d(){var b={href:e,target:u.target?u.target:null,rel:u.rel?u.rel:null,"class":u["class"]?u["class"]:null,title:u.title?u.title:null};j?(a.focus(),m&&u.text!=k&&("innerText"in j?j.innerText=u.text:j.textContent=u.text),w.setAttribs(j,b),v.select(j),a.undoManager.add()):m?a.insertContent(w.createHTML("a",b,w.encode(u.text))):a.execCommand("mceInsertLink",!1,b)}var e;return u=tinymce.extend(u,b.data),(e=u.href)?e.indexOf("@")>0&&-1==e.indexOf("//")&&-1==e.indexOf("mailto:")?void c("The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?",function(a){a&&(e="mailto:"+e),d()}):a.settings.link_assume_external_targets&&!/^\w+:/i.test(e)||!a.settings.link_assume_external_targets&&/^\s*www[\.|\d\.]/i.test(e)?void c("The URL you entered seems to be an external link. Do you want to add the required http:// prefix?",function(a){a&&(e="http://"+e),d()}):void d():void a.execCommand("unlink")}})}a.addButton("link",{icon:"link",tooltip:"Insert/edit link",shortcut:"Meta+K",onclick:b(d),stateSelector:"a[href]"}),a.addButton("unlink",{icon:"unlink",tooltip:"Remove link",cmd:"unlink",stateSelector:"a[href]"}),a.addShortcut("Meta+K","",b(d)),a.addCommand("mceLink",b(d)),this.showDialog=d,a.addMenuItem("link",{icon:"link",text:"Insert/edit link",shortcut:"Meta+K",onclick:b(d),stateSelector:"a[href]",context:"insert",prependToContext:!0})}); -------------------------------------------------------------------------------- /template/admin/admin_account.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin_base.html' %} 2 | 3 | {% block admin_content %} 4 | 23 | 24 | 25 | 57 | 58 | 59 | 88 | {% end %} 89 | -------------------------------------------------------------------------------- /template/_article_comments.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if comments_pager and comments_pager.result %} 3 | {% for comment in comments_pager.result %} 4 |
  • 5 | {% if comment.comment_type == Constants.COMMENT_TYPE_REPLY %} 6 |

    7 | 8 | 回复给{{ comment.reply_to_floor }}: 9 |

    10 | {% end %} 11 |
    12 | 13 | {{comment.floor}}楼 14 |
    15 |
    16 |
    {{ comment.create_time.strftime("%Y年%m月%d日 %H:%M:%S") }}
    17 |
    18 | {{ comment.author_name }} 19 | {% if comment.rank == Constants.COMMENT_RANK_ADMIN %} 20 | 管理员 21 | {% end %} 22 |
    23 |
    24 | {% if not comment.disabled or current_user %} 25 |

    {{ comment.content }}

    26 | {% end %} 27 | {% if comment.disabled and current_user %} 28 |

    29 | 30 | 该评论已经被管理员屏蔽!访客无法查看和回复此评论内容。 31 |

    32 | {% elif comment.disabled %} 33 |

    34 | 35 | 该评论已经被管理员屏蔽! 36 |

    37 | {% end %} 38 |
    39 |
    40 |
    41 | {% if current_user %} 42 |
    43 | {% if not comment.disabled %} 44 | 47 | {% else %} 48 | 51 | {% end %} 52 |
    53 |
    54 | 57 |
    58 | {% end %} 59 | {% if not comment.disabled or current_user %} 60 |
    61 | 64 |
    65 | {% end %} 66 |
    67 |
  • 68 | {% end %} 69 | {% else %} 70 |
  • 71 |
    暂无评论
    72 |
  • 73 | {% end %} 74 |
75 | 76 | 77 | -------------------------------------------------------------------------------- /template/admin/custom_blog_plugin.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin_base.html' %} 2 | 3 | {% block title2 %} 4 | 插件管理 5 | {% end %} 6 | 7 | {% block admin_content %} 8 |
9 |

插件管理

10 |
11 |

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

12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for plugin in pager.result %} 28 | 29 | 30 | 31 | 32 | 42 | 57 | {% if plugin.content != 'system_plugin' %} 58 | 64 | 70 | {% else %} 71 | 72 | 73 | {% end %} 74 | 75 | {% end %} 76 | 77 |
序号插件名称备注排序启用修改删除
{{ plugin.order }}{{ plugin.title }}{{ plugin.note }} 33 | 35 | 36 | 37 | 39 | 40 | 41 | 43 | {% if plugin.disabled %} 44 | 47 | 48 | 49 | {% else %} 50 | 53 | 54 | 55 | {% end %} 56 | 59 | 61 | 62 | 63 | 65 | 67 | 68 | 69 |
78 |
79 |
80 | {% module Template("_macros.html", pager=pager, url=reverse_url('admin.custom.blog_plugin'), params=None) %} 81 |
82 | 83 | 84 | 107 | {% end %} 108 | -------------------------------------------------------------------------------- /static/tinymce/js/tinymce/plugins/visualblocks/css/visualblocks.css: -------------------------------------------------------------------------------- 1 | .mce-visualblocks p { 2 | padding-top: 10px; 3 | border: 1px dashed #BBB; 4 | margin-left: 3px; 5 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7); 6 | } 7 | 8 | .mce-visualblocks h1 { 9 | padding-top: 10px; 10 | border: 1px dashed #BBB; 11 | margin-left: 3px; 12 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==); 13 | } 14 | 15 | .mce-visualblocks h2 { 16 | padding-top: 10px; 17 | border: 1px dashed #BBB; 18 | margin-left: 3px; 19 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==); 20 | } 21 | 22 | .mce-visualblocks h3 { 23 | padding-top: 10px; 24 | border: 1px dashed #BBB; 25 | margin-left: 3px; 26 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7); 27 | } 28 | 29 | .mce-visualblocks h4 { 30 | padding-top: 10px; 31 | border: 1px dashed #BBB; 32 | margin-left: 3px; 33 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==); 34 | } 35 | 36 | .mce-visualblocks h5 { 37 | padding-top: 10px; 38 | border: 1px dashed #BBB; 39 | margin-left: 3px; 40 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==); 41 | } 42 | 43 | .mce-visualblocks h6 { 44 | padding-top: 10px; 45 | border: 1px dashed #BBB; 46 | margin-left: 3px; 47 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==); 48 | } 49 | 50 | .mce-visualblocks div { 51 | padding-top: 10px; 52 | border: 1px dashed #BBB; 53 | margin-left: 3px; 54 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7); 55 | } 56 | 57 | .mce-visualblocks section { 58 | padding-top: 10px; 59 | border: 1px dashed #BBB; 60 | margin: 0 0 1em 3px; 61 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=); 62 | } 63 | 64 | .mce-visualblocks article { 65 | padding-top: 10px; 66 | border: 1px dashed #BBB; 67 | margin: 0 0 1em 3px; 68 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7); 69 | } 70 | 71 | .mce-visualblocks blockquote { 72 | padding-top: 10px; 73 | border: 1px dashed #BBB; 74 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7); 75 | } 76 | 77 | .mce-visualblocks address { 78 | padding-top: 10px; 79 | border: 1px dashed #BBB; 80 | margin: 0 0 1em 3px; 81 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=); 82 | } 83 | 84 | .mce-visualblocks pre { 85 | padding-top: 10px; 86 | border: 1px dashed #BBB; 87 | margin-left: 3px; 88 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==); 89 | } 90 | 91 | .mce-visualblocks figure { 92 | padding-top: 10px; 93 | border: 1px dashed #BBB; 94 | margin: 0 0 1em 3px; 95 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7); 96 | } 97 | 98 | .mce-visualblocks hgroup { 99 | padding-top: 10px; 100 | border: 1px dashed #BBB; 101 | margin: 0 0 1em 3px; 102 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7); 103 | } 104 | 105 | .mce-visualblocks aside { 106 | padding-top: 10px; 107 | border: 1px dashed #BBB; 108 | margin: 0 0 1em 3px; 109 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=); 110 | } 111 | 112 | .mce-visualblocks figcaption { 113 | border: 1px dashed #BBB; 114 | } 115 | 116 | .mce-visualblocks ul { 117 | padding-top: 10px; 118 | border: 1px dashed #BBB; 119 | margin: 0 0 1em 3px; 120 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==) 121 | } 122 | 123 | .mce-visualblocks ol { 124 | padding-top: 10px; 125 | border: 1px dashed #BBB; 126 | margin: 0 0 1em 3px; 127 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==); 128 | } 129 | 130 | .mce-visualblocks dl { 131 | padding-top: 10px; 132 | border: 1px dashed #BBB; 133 | margin: 0 0 1em 3px; 134 | background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==); 135 | } 136 | --------------------------------------------------------------------------------