├── .gitignore ├── README.md ├── babel.cfg ├── manager.py ├── messages.pot └── pypress ├── __init__.py ├── database.py ├── extensions ├── __init__.py ├── cache.py ├── forms.py ├── permission.py ├── routing.py ├── sessions.py ├── signals.py └── sqlalchemy.py ├── filters.py ├── forms.py ├── helpers.py ├── local_settings.py ├── models ├── __init__.py ├── blog.py ├── links.py ├── types.py └── users.py ├── permissions.py ├── settings.py ├── static └── js │ ├── jquery.min.js │ └── uEditor │ ├── img │ ├── b.png │ ├── bg_button.png │ ├── bgcolor.png │ ├── close.png │ ├── code.png │ ├── color.png │ ├── del.png │ ├── i.png │ ├── image.png │ ├── indent.png │ ├── justifycenter.png │ ├── justifyleft.png │ ├── justifyright.png │ ├── link.png │ ├── ol.png │ ├── outdent.png │ ├── pagebreaks.png │ ├── source.png │ ├── strikethrough.png │ ├── titlebg.png │ ├── u.png │ ├── ul.png │ └── underline.png │ ├── origin.js │ ├── uEditor.css │ ├── uEditor.js │ ├── uEditorContent.css │ └── uEditor_datauri.css ├── themes ├── default │ ├── static │ │ ├── base.css │ │ ├── bg.gif │ │ ├── blog.js │ │ ├── glass-light.png │ │ ├── highlight.css │ │ └── post-line.png │ └── templates │ │ ├── account │ │ ├── login.html │ │ └── signup.html │ │ ├── base.html │ │ ├── blog │ │ ├── add_comment.html │ │ ├── archives.html │ │ ├── edit.html │ │ ├── list.html │ │ ├── module-comment.html │ │ ├── module-post.html │ │ ├── people.html │ │ ├── post.html │ │ ├── search.html │ │ ├── tags.html │ │ └── view.html │ │ ├── errors │ │ ├── 404.html │ │ ├── 500.html │ │ └── exception.htm │ │ └── macros │ │ ├── _field_errors.html │ │ └── _page.html └── simple │ ├── static │ ├── base.css │ ├── bg.gif │ ├── blog.js │ ├── find.png │ ├── glass-light.png │ └── highlight.css │ └── templates │ ├── account │ ├── login.html │ └── signup.html │ ├── base.html │ ├── blog │ ├── add_comment.html │ ├── archives.html │ ├── edit.html │ ├── list.html │ ├── module-comment.html │ ├── module-list.html │ ├── module-post.html │ ├── people.html │ ├── post.html │ ├── search.html │ ├── tag.html │ ├── tags.html │ └── view.html │ ├── errors │ ├── 404.html │ ├── 500.html │ └── exception.htm │ ├── links │ ├── add.html │ └── list.html │ └── macros │ ├── _field_errors.html │ └── _page.html ├── translations └── zh_CN │ └── LC_MESSAGES │ ├── messages.mo │ └── messages.po ├── uimodules.py ├── utils ├── .DS_Store ├── FacesAndCaps.ttf ├── __init__.py └── imagelib.py └── views ├── __init__.py ├── account.py ├── base.py ├── blog.py └── links.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.pyo 4 | *.egg-info 5 | *.bak 6 | *.sql 7 | *.pid 8 | *.db 9 | *.sw[po] 10 | local* 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pypress, 由python编写的团队博客 2 | =========================== 3 | 4 | ### 最近改动 5 | * 2012-3-24: 加入第二套皮肤 simple 6 | * 2012-3-25: 完善侧栏最后评论功能 7 | * 2012-3-26: 完善友情链接模块功能 8 | * 2012-3-30: 加入session机制 9 | * 支持redis存储或使用tornado的secure_cookie 10 | * 使用不用方法时,只需要设置settings的REDIS_SERVER开关   11 | * 支持session有效期的设置,可使用PERMANENT_SESSION_LIFETIME 12 | 13 | 14 | ### 项目介绍 15 | 16 | 原先版本基于flask框架 17 | 18 | flask是一个很不错的框架,写项目比较轻松,较多高质量的插件,帮助python新手能快速创建自己的项目。 19 | 而且也有不少国外项目可以参考,比如newsmeme,原先的pypress就是学习的这个项目而创建的,非常受用。 20 | 21 | pypress原也是自己的学习项目,没想到还有许多朋友来邮件表示在用,原想在flask上继续更新,但又学习上了tornado,于是直接使用tornado重写了。 22 | 23 | 说下tornado,是一个不错的server,但做框架,还是缺少太多东西,写了一段时间tornado,我已经开始怀念flask的高效了.... 24 | 25 | 反正是学习,我将flask自己比较中意的插件,应用在了tornado上,并做了相应修改: 26 | 27 | 1. sqlalchemy 28 | 改动最大,比如分页类(Pagination),以及_SinalTrackingMapperExtension类_record输入处理等 29 | 30 | 2. wtforms 31 | 只是简单的对tornado的request进行了处理 32 | 33 | 3. cache 34 | 这是从werkzeug源码里copy的类进行了修改 35 | 36 | 4. signals 37 | 这是纯从flask里拿来的,只是对blinker的导入进行了简单处理而已 38 | 39 | 5. routing 40 | 这是为tornado的反向路由写的一个比较简单的类,解决了tornado只在handler里使用reverse_url的问题 41 | 42 | 6. permission 43 | 这个也是纯从flask里拿来的,只是做了一点点小改动,让它能在tornado里使用 44 | 45 | 46 | 而pypress-tornado,目前没有原先pypress功能丰富,主要改动: 47 | 48 | 1. 换了个编辑器,从国外一个简单的demo基础上改进而来,也许会有一些bug :( 49 | 50 | 2. 加了评论验证码 51 | 52 | 3. 去除了twitter相关内容 53 | 54 | 项目演示: [laoqiu.com](http://laoqiu.com/ "laoqiu blog") 55 | 56 | 57 | -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | [python: **/templates/**.html] 3 | -------------------------------------------------------------------------------- /manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | import uuid 5 | 6 | import tornado.httpserver 7 | import tornado.ioloop 8 | import tornado.options 9 | 10 | from tornado.options import define, options 11 | 12 | from pypress import Application 13 | from pypress.models import * 14 | from pypress.database import db 15 | 16 | define("cmd", default='runserver', 17 | metavar="runserver|createall|dropall|createcode", 18 | help=("Default use runserver")) 19 | define("port", default=9000, help="default: 9000, required runserver", type=int) 20 | define("num", default=1, help="number of create codes. required createcode", type=int) 21 | define("role", default='admin', metavar="admin|moderator|member", help="required createcode") 22 | 23 | def main(): 24 | tornado.options.parse_command_line() 25 | 26 | if options.cmd == 'runserver': 27 | print 'server started. port %s' % options.port 28 | http_server = tornado.httpserver.HTTPServer(Application()) 29 | http_server.listen(options.port) 30 | tornado.ioloop.IOLoop.instance().start() 31 | 32 | elif options.cmd == 'createall': 33 | "Creates database tables" 34 | db.create_all() 35 | print 'create all [ok]' 36 | 37 | elif options.cmd == 'dropall': 38 | "Drops all database tables" 39 | db.drop_all() 40 | print 'drop all [ok]' 41 | 42 | elif options.cmd == 'createcode': 43 | codes = [] 44 | usercodes = [] 45 | for i in range(options.num): 46 | code = unicode(uuid.uuid4()).split('-')[0] 47 | codes.append(code) 48 | usercode = UserCode() 49 | usercode.code = code 50 | if options.role == "admin": 51 | usercode.role = User.ADMIN 52 | elif options.role == "moderator": 53 | usercode.role = User.MODERATOR 54 | else: 55 | usercode.role = User.MEMBER 56 | usercodes.append(usercode) 57 | if options.num==1: 58 | db.session.add(usercode) 59 | else: 60 | db.session.add_all(usercodes) 61 | db.session.commit() 62 | print "Sign up code:" 63 | for i in codes: 64 | print i 65 | 66 | else: 67 | print 'error cmd param: python manager.py --help' 68 | 69 | if __name__=='__main__': 70 | main() 71 | 72 | -------------------------------------------------------------------------------- /messages.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2012 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2012. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2012-03-23 17:09+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.6\n" 19 | 20 | #: pypress/forms.py:27 21 | msgid "You can only use letters, numbers or dashes" 22 | msgstr "" 23 | 24 | #: pypress/forms.py:32 25 | msgid "Username or email" 26 | msgstr "" 27 | 28 | #: pypress/forms.py:34 29 | msgid "You must provide an email or username" 30 | msgstr "" 31 | 32 | #: pypress/forms.py:36 pypress/forms.py:53 pypress/forms.py:89 33 | msgid "Password" 34 | msgstr "" 35 | 36 | #: pypress/forms.py:38 37 | msgid "Remember me" 38 | msgstr "" 39 | 40 | #: pypress/forms.py:42 pypress/themes/default/templates/account/login.html:2 41 | msgid "Login" 42 | msgstr "" 43 | 44 | #: pypress/forms.py:46 45 | msgid "Username" 46 | msgstr "" 47 | 48 | #: pypress/forms.py:47 49 | msgid "Username required" 50 | msgstr "" 51 | 52 | #: pypress/forms.py:50 pypress/forms.py:145 53 | msgid "Nickname" 54 | msgstr "" 55 | 56 | #: pypress/forms.py:51 pypress/forms.py:146 57 | msgid "Nickname required" 58 | msgstr "" 59 | 60 | #: pypress/forms.py:54 61 | msgid "Password required" 62 | msgstr "" 63 | 64 | #: pypress/forms.py:56 pypress/forms.py:95 65 | msgid "Password again" 66 | msgstr "" 67 | 68 | #: pypress/forms.py:58 pypress/forms.py:97 69 | msgid "Passwords don't match" 70 | msgstr "" 71 | 72 | #: pypress/forms.py:60 pypress/forms.py:141 pypress/forms.py:169 73 | msgid "Email" 74 | msgstr "" 75 | 76 | #: pypress/forms.py:61 pypress/forms.py:142 77 | msgid "Email required" 78 | msgstr "" 79 | 80 | #: pypress/forms.py:62 pypress/forms.py:83 pypress/forms.py:143 81 | #: pypress/forms.py:170 82 | msgid "A valid email is required" 83 | msgstr "" 84 | 85 | #: pypress/forms.py:64 86 | msgid "Signup Code" 87 | msgstr "" 88 | 89 | #: pypress/forms.py:68 pypress/themes/default/templates/account/signup.html:2 90 | msgid "Signup" 91 | msgstr "" 92 | 93 | #: pypress/forms.py:73 94 | msgid "This username is taken" 95 | msgstr "" 96 | 97 | #: pypress/forms.py:78 98 | msgid "This email is taken" 99 | msgstr "" 100 | 101 | #: pypress/forms.py:82 102 | msgid "Your email address" 103 | msgstr "" 104 | 105 | #: pypress/forms.py:85 106 | msgid "Find password" 107 | msgstr "" 108 | 109 | #: pypress/forms.py:90 110 | msgid "Password is required" 111 | msgstr "" 112 | 113 | #: pypress/forms.py:92 114 | msgid "New Password" 115 | msgstr "" 116 | 117 | #: pypress/forms.py:93 118 | msgid "New Password is required" 119 | msgstr "" 120 | 121 | #: pypress/forms.py:99 pypress/forms.py:120 pypress/forms.py:178 122 | #: pypress/forms.py:185 123 | msgid "Save" 124 | msgstr "" 125 | 126 | #: pypress/forms.py:103 127 | msgid "Recaptcha" 128 | msgstr "" 129 | 130 | #: pypress/forms.py:105 131 | msgid "Delete" 132 | msgstr "" 133 | 134 | #: pypress/forms.py:109 135 | msgid "Title" 136 | msgstr "" 137 | 138 | #: pypress/forms.py:110 139 | msgid "Title required" 140 | msgstr "" 141 | 142 | #: pypress/forms.py:112 143 | msgid "Slug" 144 | msgstr "" 145 | 146 | #: pypress/forms.py:114 147 | msgid "Content" 148 | msgstr "" 149 | 150 | #: pypress/forms.py:115 151 | msgid "Content required" 152 | msgstr "" 153 | 154 | #: pypress/forms.py:117 pypress/themes/default/templates/base.html:47 155 | msgid "Tags" 156 | msgstr "" 157 | 158 | #: pypress/forms.py:118 159 | msgid "Tags required" 160 | msgstr "" 161 | 162 | #: pypress/forms.py:130 163 | msgid "Slug must be less than 50 characters" 164 | msgstr "" 165 | 166 | #: pypress/forms.py:136 167 | msgid "This slug is taken" 168 | msgstr "" 169 | 170 | #: pypress/forms.py:136 171 | msgid "Slug is required" 172 | msgstr "" 173 | 174 | #: pypress/forms.py:148 175 | msgid "Website" 176 | msgstr "" 177 | 178 | #: pypress/forms.py:150 pypress/forms.py:167 pypress/forms.py:174 179 | msgid "A valid url is required" 180 | msgstr "" 181 | 182 | #: pypress/forms.py:152 183 | msgid "Comment" 184 | msgstr "" 185 | 186 | #: pypress/forms.py:153 187 | msgid "Comment required" 188 | msgstr "" 189 | 190 | #: pypress/forms.py:155 191 | msgid "Captcha" 192 | msgstr "" 193 | 194 | #: pypress/forms.py:156 195 | msgid "Captcha required" 196 | msgstr "" 197 | 198 | #: pypress/forms.py:158 199 | #: pypress/themes/default/templates/blog/add_comment.html:2 200 | msgid "Add comment" 201 | msgstr "" 202 | 203 | #: pypress/forms.py:159 pypress/forms.py:186 204 | msgid "Cancel" 205 | msgstr "" 206 | 207 | #: pypress/forms.py:163 208 | msgid "Site name" 209 | msgstr "" 210 | 211 | #: pypress/forms.py:164 212 | msgid "Site name required" 213 | msgstr "" 214 | 215 | #: pypress/forms.py:166 216 | msgid "link" 217 | msgstr "" 218 | 219 | #: pypress/forms.py:172 220 | msgid "Logo" 221 | msgstr "" 222 | 223 | #: pypress/forms.py:176 224 | msgid "Description" 225 | msgstr "" 226 | 227 | #: pypress/forms.py:182 228 | msgid "HTML" 229 | msgstr "" 230 | 231 | #: pypress/forms.py:183 232 | msgid "HTML required" 233 | msgstr "" 234 | 235 | #: pypress/themes/default/templates/base.html:50 236 | msgid "Archives" 237 | msgstr "" 238 | 239 | #: pypress/themes/default/templates/base.html:53 240 | #: pypress/themes/default/templates/blog/post.html:2 241 | msgid "Post Now" 242 | msgstr "" 243 | 244 | #: pypress/themes/default/templates/base.html:60 245 | msgid "login" 246 | msgstr "" 247 | 248 | #: pypress/themes/default/templates/blog/edit.html:2 249 | msgid "Edit" 250 | msgstr "" 251 | 252 | #: pypress/themes/default/templates/blog/list.html:2 253 | msgid "List" 254 | msgstr "" 255 | 256 | #: pypress/themes/default/templates/blog/module-comment.html:21 257 | msgid "Are you sure you want to delete this comment ?" 258 | msgstr "" 259 | 260 | #: pypress/themes/default/templates/blog/module-comment.html:22 261 | msgid "yes" 262 | msgstr "" 263 | 264 | #: pypress/themes/default/templates/blog/module-comment.html:23 265 | msgid "no" 266 | msgstr "" 267 | 268 | #: pypress/themes/default/templates/blog/view.html:23 269 | msgid "edit" 270 | msgstr "" 271 | 272 | #: pypress/themes/default/templates/blog/view.html:87 273 | msgid "No comments have been posted yet." 274 | msgstr "" 275 | 276 | #: pypress/views/account.py:42 277 | #, python-format 278 | msgid "Welcome back, %s" 279 | msgstr "" 280 | 281 | #: pypress/views/account.py:51 282 | msgid "The username or password you provided are incorrect." 283 | msgstr "" 284 | 285 | #: pypress/views/account.py:106 286 | msgid "Code is not allowed" 287 | msgstr "" 288 | 289 | #: pypress/views/blog.py:195 290 | msgid "Captcha don't match" 291 | msgstr "" 292 | 293 | -------------------------------------------------------------------------------- /pypress/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | pypress Application 5 | ~~~~~~~~~~~ 6 | :author: laoqiu.com@gmail.com 7 | """ 8 | import os 9 | import redis 10 | 11 | import tornado.web 12 | import tornado.locale 13 | 14 | from tornado.web import url 15 | 16 | from pypress import settings as config 17 | from pypress import uimodules 18 | from pypress.helpers import setting_from_object 19 | from pypress.forms import create_forms 20 | from pypress.views import account, blog, links, ErrorHandler 21 | from pypress.database import db, models_committed 22 | from pypress.extensions.routing import Route 23 | from pypress.extensions.sessions import RedisSessionStore 24 | 25 | class Application(tornado.web.Application): 26 | def __init__(self): 27 | settings = setting_from_object(config) 28 | handlers = [ 29 | # other handlers... 30 | url(r"/theme/static/(.+)", tornado.web.StaticFileHandler, dict(path=settings['theme_static_path']), name='theme_static'), 31 | url(r"/upload/(.+)", tornado.web.StaticFileHandler, dict(path=settings['upload_path']), name='upload_path') 32 | ] + Route.routes() 33 | 34 | # Custom 404 ErrorHandler 35 | handlers.append((r"/(.*)", ErrorHandler)) 36 | 37 | settings.update(dict( 38 | ui_modules = uimodules, 39 | autoescape = None 40 | )) 41 | 42 | if 'default_locale' in settings: 43 | tornado.locale.load_gettext_translations( 44 | os.path.join(os.path.dirname(__file__), 'translations'), 'messages') 45 | 46 | tornado.web.Application.__init__(self, handlers, **settings) 47 | 48 | self.forms = create_forms() 49 | pool = redis.ConnectionPool(host=settings['redis_host'], port=settings['redis_port'], db=settings['redis_db']) 50 | self.redis = redis.Redis(connection_pool=pool) 51 | self.session_store = RedisSessionStore(self.redis) 52 | 53 | configure_signals(db.sender) 54 | 55 | 56 | def configure_signals(sender): 57 | 58 | @models_committed.connect_via(sender) 59 | def on_models_commited(sender, changes): 60 | # print sender 61 | pass 62 | 63 | 64 | -------------------------------------------------------------------------------- /pypress/database.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | import settings 4 | 5 | from extensions.sqlalchemy import SQLAlchemy, BaseQuery, \ 6 | models_committed, before_models_committed 7 | 8 | db = SQLAlchemy(settings.SQLALCHEMY_DATABASE_URI, settings.SQLALCHEMY_DATABASE_ECHO) 9 | 10 | -------------------------------------------------------------------------------- /pypress/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /pypress/extensions/cache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | extensions.cache 5 | 6 | Example: 7 | >>> from extensions.cache import SimpleCache 8 | >>> c = SimpleCahce() 9 | >>> c.set('a', 'xx') 10 | >>> c.get('a') 11 | 'xx' 12 | >>> c.get('b') is None 13 | True 14 | """ 15 | import hashlib 16 | from time import time 17 | from itertools import izip 18 | from functools import wraps 19 | try: 20 | import cPickle as pickle 21 | except ImportError: 22 | import pickle 23 | 24 | 25 | class BaseCache(object): 26 | def __init__(self, timeout=300): 27 | self.timeout = timeout 28 | 29 | def get(self, key): 30 | return None 31 | 32 | def get_many(self, *keys): 33 | return map(self.get, keys) 34 | 35 | def get_dict(self, *keys): 36 | return dict(izip(keys, self.get_many(*keys))) 37 | 38 | def set(self, key, value, timeout=None): 39 | pass 40 | 41 | def delete(self, key): 42 | pass 43 | 44 | def clear(self): 45 | pass 46 | 47 | def mark_key(self, function, args, kwargs): 48 | try: 49 | key = pickle.dumps((function.func_name, args, kwargs)) 50 | except: 51 | key = pickle.dumps(function.func_name) 52 | return hashlib.sha1(key).hexdigest() 53 | 54 | def cached(self, timeout=None, unless=None): 55 | """ 56 | Example: 57 | """ 58 | def decorator(f): 59 | @wraps(f) 60 | def decorated_function(*args, **kwargs): 61 | if callable(unless) and unless() is True: 62 | return f(*args, **kwargs) 63 | 64 | key = self.mark_key(f, args, kwargs) 65 | 66 | rv = self.get(key) 67 | 68 | if rv is None: 69 | rv = f(*args, **kwargs) 70 | self.set(key, rv, timeout=timeout) 71 | 72 | return rv 73 | return decorated_function 74 | return decorator 75 | 76 | 77 | class SimpleCache(BaseCache): 78 | def __init__(self, threshold=500, timeout=300): 79 | BaseCache.__init__(self, timeout) 80 | self._cache = {} 81 | self._threshold = threshold 82 | 83 | def _prune(self): 84 | if len(self._cache) >= self._threshold: 85 | num = len(self._cache) - self._threshold + 1 86 | for key, value in sorted(self._cache.items(), key=lambda x:x[1][0])[:num]: 87 | self._cache.pop(key, None) 88 | 89 | def get(self, key): 90 | expires, value = self._cache.get(key, (0, None)) 91 | if expires > time(): 92 | return pickle.loads(value) 93 | 94 | def set(self, key, value, timeout=None): 95 | if timeout is None: 96 | timeout = self.timeout 97 | self._prune() 98 | self._cache[key] = (time() + timeout, pickle.dumps(value, 99 | pickle.HIGHEST_PROTOCOL)) 100 | 101 | def delete(self, key): 102 | self._cache.pop(key, None) 103 | 104 | def clear(self): 105 | for key, (expires, _) in self._cache.iteritems(): 106 | if expires < time(): 107 | self._cache.pop(key, None) 108 | 109 | 110 | cache = SimpleCache() 111 | 112 | class cached_property(object): 113 | def __init__(self, func, name=None): 114 | self.__name__ = name or func.__name__ 115 | self.__module__ = func.__module__ 116 | self.func = func 117 | 118 | def __get__(self, obj, type=None): 119 | if obj is None: 120 | return self 121 | value = obj.__dict__.get(self.__name__, None) 122 | if value is None: 123 | value = self.func(obj) 124 | obj.__dict__[self.__name__] = value 125 | return value 126 | 127 | 128 | -------------------------------------------------------------------------------- /pypress/extensions/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | forms.py 5 | ~~~~~~~~~~~~~ 6 | wtforms extensions for tornado 7 | """ 8 | import re 9 | 10 | from tornado.escape import _unicode 11 | 12 | from wtforms import Form as BaseForm, fields, validators, widgets, ext 13 | 14 | from wtforms.fields import BooleanField, DecimalField, DateField, \ 15 | DateTimeField, FieldList, FloatField, FormField, \ 16 | HiddenField, IntegerField, PasswordField, RadioField, SelectField, \ 17 | SelectMultipleField, SubmitField, TextField, TextAreaField 18 | 19 | from wtforms.validators import ValidationError, Email, email, EqualTo, equal_to, \ 20 | IPAddress, ip_address, Length, length, NumberRange, number_range, \ 21 | Optional, optional, Required, required, Regexp, regexp, \ 22 | URL, url, AnyOf, any_of, NoneOf, none_of 23 | 24 | from wtforms.widgets import CheckboxInput, FileInput, HiddenInput, \ 25 | ListWidget, PasswordInput, RadioInput, Select, SubmitInput, \ 26 | TableWidget, TextArea, TextInput 27 | 28 | try: 29 | import sqlalchemy 30 | _is_sqlalchemy = True 31 | except ImportError: 32 | _is_sqlalchemy = False 33 | 34 | 35 | if _is_sqlalchemy: 36 | from wtforms.ext.sqlalchemy.fields import QuerySelectField, \ 37 | QuerySelectMultipleField 38 | 39 | for field in (QuerySelectField, 40 | QuerySelectMultipleField): 41 | 42 | setattr(fields, field.__name__, field) 43 | 44 | 45 | class Form(BaseForm): 46 | """ 47 | Example: 48 | >>> user = User.query.get(1) 49 | >>> form = LoginForm(user) 50 | {{ xsrf_form_html }} 51 | {{ form.hiden_tag() }} 52 | {{ form.username }} 53 | """ 54 | 55 | def __init__(self, formdata=None, *args, **kwargs): 56 | self.obj = kwargs.get('obj', None) 57 | super(Form, self).__init__(formdata, *args, **kwargs) 58 | 59 | def process(self, formdata=None, *args, **kwargs): 60 | if formdata is not None and not hasattr(formdata, 'getlist'): 61 | formdata = TornadoInputWrapper(formdata) 62 | super(Form, self).process(formdata, *args, **kwargs) 63 | 64 | def hidden_tag(self, *fields): 65 | """ 66 | Wraps hidden fields in a hidden DIV tag, in order to keep XHTML 67 | compliance. 68 | """ 69 | 70 | if not fields: 71 | fields = [f for f in self if isinstance(f, HiddenField)] 72 | 73 | rv = [] 74 | for field in fields: 75 | if isinstance(field, basestring): 76 | field = getattr(self, field) 77 | rv.append(unicode(field)) 78 | 79 | return u"".join(rv) 80 | 81 | 82 | class TornadoInputWrapper(dict): 83 | """ 84 | From tornado source-> RequestHandler.get_arguments 85 | """ 86 | def getlist(self, name, strip=True): 87 | values = [] 88 | for v in self.get(name, []): 89 | v = _unicode(v) 90 | if isinstance(v, unicode): 91 | # Get rid of any weird control chars (unless decoding gave 92 | # us bytes, in which case leave it alone) 93 | v = re.sub(r"[\x00-\x08\x0e-\x1f]", " ", v) 94 | if strip: 95 | v = v.strip() 96 | values.append(v) 97 | return values 98 | 99 | 100 | -------------------------------------------------------------------------------- /pypress/extensions/permission.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | extensions: permission.py 5 | permission for tornado. from flask-principal. 6 | :modified by laoqiu.com@gmail.com 7 | 8 | Example: 9 | >>> from extensions.permission import UserNeed, RoleNeed, ItemNeed, Permission 10 | >>> admin = Permission(RoleNeed('admin')) 11 | >>> editor = Permission(UserNeed(1)) & admin 12 | 13 | # handers 14 | ~~~~~~~~~ 15 | 16 | @admin.require(401) 17 | def get(self): 18 | self.write('is admin') 19 | return 20 | 21 | def post(self): 22 | # or 23 | if editor.can(self.identity): 24 | print 'admin' 25 | # or 26 | editor.test(self.identity, 401) 27 | return 28 | 29 | """ 30 | import sys 31 | import tornado.web 32 | 33 | from functools import wraps, partial 34 | from collections import namedtuple 35 | 36 | __all__ = ['UserNeed', 'RoleNeed', 'ItemNeed', 'Permission', 'Identity', 'AnonymousIdentity'] 37 | 38 | Need = namedtuple('Need', ['method', 'value']) 39 | 40 | UserNeed = partial(Need, 'user') 41 | RoleNeed = partial(Need, 'role') 42 | ItemNeed = namedtuple('ItemNeed', ['method', 'value', 'type']) 43 | 44 | 45 | class PermissionDenied(RuntimeError): 46 | """Permission denied to the resource""" 47 | pass 48 | 49 | 50 | class Identity(object): 51 | """ 52 | A set of needs provided by this user 53 | 54 | example: 55 | identity = Identity('ali') 56 | identity.provides.add(('role', 'admin')) 57 | """ 58 | def __init__(self, name): 59 | self.name = name 60 | self.provides = set() 61 | 62 | def can(self, permission): 63 | return permission.allows(self) 64 | 65 | 66 | class AnonymousIdentity(Identity): 67 | """An anonymous identity 68 | :attr name: "anonymous" 69 | """ 70 | def __init__(self): 71 | Identity.__init__(self, 'anonymous') 72 | 73 | 74 | class IdentityContext(object): 75 | """The context of an identity for a permission. 76 | 77 | .. note:: The principal is usually created by the flaskext.Permission.require method 78 | call for normal use-cases. 79 | 80 | The principal behaves as either a context manager or a decorator. The 81 | permission is checked for provision in the identity, and if available the 82 | flow is continued (context manager) or the function is executed (decorator). 83 | """ 84 | 85 | def __init__(self, permission, http_exception=None, identity=None): 86 | self.permission = permission 87 | self.http_exception = http_exception 88 | self.identity = identity if identity else AnonymousIdentity() 89 | 90 | def can(self): 91 | """Whether the identity has access to the permission 92 | """ 93 | return self.identity.can(self.permission) 94 | 95 | def __call__(self, method): 96 | @wraps(method) 97 | def wrapper(handler, *args, **kwargs): 98 | self.identity = handler.identity 99 | self.__enter__() 100 | exc = (None, None, None) 101 | try: 102 | result = method(handler, *args, **kwargs) 103 | except Exception: 104 | exc = sys.exc_info() 105 | self.__exit__(*exc) 106 | return result 107 | return wrapper 108 | 109 | def __enter__(self): 110 | # check the permission here 111 | if not self.can(): 112 | if self.http_exception: 113 | raise tornado.web.HTTPError(self.http_exception) 114 | raise PermissionDenied(self.permission) 115 | 116 | def __exit__(self, *exc): 117 | if exc != (None, None, None): 118 | cls, val, tb = exc 119 | raise cls, val, tb 120 | return False 121 | 122 | 123 | class Permission(object): 124 | """ 125 | Represents needs, any of which must be present to access a resource 126 | :param needs: The needs for this permission 127 | """ 128 | def __init__(self, *needs): 129 | self.needs = set(needs) 130 | self.excludes = set() 131 | 132 | def __and__(self, other): 133 | """Does the same thing as "self.union(other)" 134 | """ 135 | return self.union(other) 136 | 137 | def __or__(self, other): 138 | """Does the same thing as "self.difference(other)" 139 | """ 140 | return self.difference(other) 141 | 142 | def __contains__(self, other): 143 | """Does the same thing as "other.issubset(self)". 144 | """ 145 | return other.issubset(self) 146 | 147 | def require(self, http_exception=None, identity=None): 148 | return IdentityContext(self, http_exception, identity) 149 | 150 | def test(self, identity, http_exception=None): 151 | with self.require(http_exception, identity): 152 | pass 153 | 154 | def reverse(self): 155 | """ 156 | Returns reverse of current state (needs->excludes, excludes->needs) 157 | """ 158 | 159 | p = Permission() 160 | p.needs.update(self.excludes) 161 | p.excludes.update(self.needs) 162 | return p 163 | 164 | def union(self, other): 165 | """Create a new permission with the requirements of the union of this 166 | and other. 167 | 168 | :param other: The other permission 169 | """ 170 | p = Permission(*self.needs.union(other.needs)) 171 | p.excludes.update(self.excludes.union(other.excludes)) 172 | return p 173 | 174 | def difference(self, other): 175 | """Create a new permission consisting of requirements in this 176 | permission and not in the other. 177 | """ 178 | 179 | p = Permission(*self.needs.difference(other.needs)) 180 | p.excludes.update(self.excludes.difference(other.excludes)) 181 | return p 182 | 183 | def issubset(self, other): 184 | """Whether this permission needs are a subset of another 185 | 186 | :param other: The other permission 187 | """ 188 | return self.needs.issubset(other.needs) and \ 189 | self.excludes.issubset(other.excludes) 190 | 191 | def allows(self, identity): 192 | """Whether the identity can access this permission. 193 | 194 | :param identity: The identity 195 | """ 196 | if self.needs and not self.needs.intersection(identity.provides): 197 | return False 198 | 199 | if self.excludes and self.excludes.intersection(identity.provides): 200 | return False 201 | 202 | return True 203 | 204 | def can(self, identity): 205 | """Whether the required context for this permission has access 206 | 207 | This creates an identity context and tests whether it can access this 208 | permission 209 | """ 210 | return self.require(identity=identity).can() 211 | 212 | 213 | -------------------------------------------------------------------------------- /pypress/extensions/routing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | extensions.route 5 | 6 | Example: 7 | @route(r'/', name='index') 8 | class IndexHandler(tornado.web.RequestHandler): 9 | pass 10 | 11 | class Application(tornado.web.Application): 12 | def __init__(self): 13 | handlers = [ 14 | # ... 15 | ] + Route.routes() 16 | """ 17 | 18 | from tornado.web import url 19 | 20 | class Route(object): 21 | 22 | _routes = {} 23 | 24 | def __init__(self, pattern, kwargs={}, name=None, host='.*$'): 25 | self.pattern = pattern 26 | self.kwargs = {} 27 | self.name = name 28 | self.host = host 29 | 30 | def __call__(self, handler_class): 31 | spec = url(self.pattern, handler_class, self.kwargs, name=self.name) 32 | self._routes.setdefault(self.host, []).append(spec) 33 | return handler_class 34 | 35 | @classmethod 36 | def routes(cls, application=None): 37 | if application: 38 | for host, handlers in cls._routes.items(): 39 | application.add_handlers(host, handlers) 40 | else: 41 | return reduce(lambda x,y:x+y, cls._routes.values()) if cls._routes else [] 42 | 43 | @classmethod 44 | def url_for(cls, name, *args): 45 | named_handlers = dict([(spec.name, spec) for spec in cls.routes() if spec.name]) 46 | if name in named_handlers: 47 | return named_handlers[name].reverse(*args) 48 | raise KeyError("%s not found in named urls" % name) 49 | 50 | 51 | route = Route 52 | 53 | 54 | -------------------------------------------------------------------------------- /pypress/extensions/sessions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | extensions.session 5 | ~~~~~~~~ 6 | :origin version in https://gist.github.com/1735032 7 | """ 8 | try: 9 | import cPickle as pickle 10 | except ImportError: 11 | import pickle 12 | import time 13 | import logging 14 | from uuid import uuid4 15 | 16 | 17 | class Session(object): 18 | def __init__(self, get_secure_cookie, set_secure_cookie, name='_session', expires_days=None): 19 | self.set_session = set_secure_cookie 20 | self.get_session = get_secure_cookie 21 | self.name = name 22 | self._expiry = expires_days 23 | self._dirty = False 24 | self.get_data() 25 | 26 | def get_data(self): 27 | value = self.get_session(self.name) 28 | self._data = pickle.loads(value) if value else {} 29 | 30 | def set_expires(self, days): 31 | self._expiry = days 32 | 33 | def __getitem__(self, key): 34 | return self._data[key] 35 | 36 | def __setitem__(self, key, value): 37 | self._data[key] = value 38 | self._dirty = True 39 | 40 | def __delitem__(self, key): 41 | if key in self._data: 42 | del self._data[key] 43 | self._dirty = True 44 | 45 | def __contains__(self, key): 46 | return key in self._data 47 | 48 | def __len__(self): 49 | return len(self._data) 50 | 51 | def __iter__(self): 52 | for key in self._data: 53 | yield key 54 | 55 | def __del__(self): 56 | self.save() 57 | 58 | def save(self): 59 | if self._dirty: 60 | self.set_session(self.name, pickle.dumps(self._data), expires_days=self._expiry) 61 | self._dirty = False 62 | 63 | 64 | class RedisSessionStore(object): 65 | def __init__(self, redis_connection, **options): 66 | self.options = { 67 | 'key_prefix': 'session', 68 | 'expire': 7200, 69 | } 70 | self.options.update(options) 71 | self.redis = redis_connection 72 | 73 | def prefixed(self, sid): 74 | return '%s:%s' % (self.options['key_prefix'], sid) 75 | 76 | def generate_sid(self): 77 | return uuid4().get_hex() 78 | 79 | def get_session(self, sid, name): 80 | data = self.redis.hget(self.prefixed(sid), name) 81 | session = pickle.loads(data) if data else dict() 82 | return session 83 | 84 | def set_session(self, sid, session_data, name, expiry=None): 85 | self.redis.hset(self.prefixed(sid), name, pickle.dumps(session_data)) 86 | expiry = expiry or self.options['expire'] 87 | if expiry: 88 | self.redis.expire(self.prefixed(sid), expiry) 89 | 90 | def delete_session(self, sid): 91 | self.redis.delete(self.prefixed(sid)) 92 | 93 | 94 | class RedisSession(object): 95 | def __init__(self, session_store, session_id=None, expires_days=None): 96 | self._store = session_store 97 | self._sid = session_id if session_id else self._store.generate_sid() 98 | self._dirty = False 99 | self.set_expires(expires_days) 100 | try: 101 | self._data = self._store.get_session(self._sid, 'data') 102 | except: 103 | logging.error('Can not connect Redis server.') 104 | self._data = {} 105 | 106 | def clear(self): 107 | self._store.delete_session(self._sid) 108 | 109 | @property 110 | def id(self): 111 | return self._sid 112 | 113 | def access(self, remote_ip): 114 | access_info = {'remote_ip':remote_ip, 'time':'%.6f' % time.time()} 115 | self._store.set_session( 116 | self._sid, 117 | 'last_access', 118 | pickle.dumps(access_info)) 119 | 120 | def last_access(self): 121 | access_info = self._store.get_session(self._sid, 'last_access') 122 | return pickle.loads(access_info) 123 | 124 | def set_expires(self, days): 125 | self._expiry = days * 86400 if days else None 126 | 127 | def __getitem__(self, key): 128 | return self._data[key] 129 | 130 | def __setitem__(self, key, value): 131 | self._data[key] = value 132 | self._dirty = True 133 | 134 | def __delitem__(self, key): 135 | del self._data[key] 136 | self._dirty = True 137 | 138 | def __len__(self): 139 | return len(self._data) 140 | 141 | def __contains__(self, key): 142 | return key in self._data 143 | 144 | def __iter__(self): 145 | for key in self._data: 146 | yield key 147 | 148 | def __repr__(self): 149 | return self._data.__repr__() 150 | 151 | def __del__(self): 152 | self.save() 153 | 154 | def save(self): 155 | if self._dirty: 156 | self._store.set_session(self._sid, self._data, 'data', self._expiry) 157 | self._dirty = False 158 | 159 | 160 | -------------------------------------------------------------------------------- /pypress/extensions/signals.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python 3 | #coding=utf-8 4 | """ 5 | signals.py 6 | ~~~~~~~~~~~~~ 7 | 8 | Implements signals based on blinker if available, otherwise 9 | falls silently back to a noop 10 | 11 | :copyright: (c) 2011 by Armin Ronacher. 12 | :license: BSD, see LICENSE for more details. 13 | """ 14 | try: 15 | from blinker import Namespace 16 | except ImportError: 17 | class Namespace(object): 18 | def signal(self, name, doc=None): 19 | return _FakeSignal(name, doc) 20 | 21 | class _FakeSignal(object): 22 | """If blinker is unavailable, create a fake class with the same 23 | interface that allows sending of signals but will fail with an 24 | error on anything else. Instead of doing anything on send, it 25 | will just ignore the arguments and do nothing instead. 26 | """ 27 | 28 | def __init__(self, name, doc=None): 29 | self.name = name 30 | self.__doc__ = doc 31 | 32 | def _fail(self, *args, **kwargs): 33 | raise RuntimeError('signalling support is unavailable ' 34 | 'because the blinker library is ' 35 | 'not installed.') 36 | 37 | send = lambda *args, **kwargs: None 38 | 39 | connect = disconnect = has_receivers_for = receivers_for = \ 40 | temporarily_connected_to = connected_to = _fail 41 | 42 | del _fail 43 | 44 | 45 | # _signals = Namespace() 46 | 47 | # comment_saved = _signals.signal('comment-saved') 48 | 49 | -------------------------------------------------------------------------------- /pypress/filters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | import tornado.template 5 | 6 | 7 | def field_errors(field): 8 | t = tornado.template.Template(""" 9 | {% if field.errors %} 10 |
    11 | {% for error in field.errors %} 12 |
  • {{ error }}
  • 13 | {% end %} 14 |
15 | {% end %} 16 | """) 17 | return t.generate(field=field) 18 | 19 | -------------------------------------------------------------------------------- /pypress/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | import tornado.locale 4 | 5 | from pypress.extensions.forms import Form, TextField, PasswordField, SubmitField, \ 6 | TextAreaField, BooleanField, HiddenField, ValidationError, \ 7 | required, regexp, equal_to, email, optional, url 8 | 9 | from pypress.models import User, Post 10 | from pypress.helpers import slugify 11 | from pypress.database import db 12 | 13 | USERNAME_RE = r'^[\w.+-]+$' 14 | 15 | 16 | def create_forms(): 17 | 18 | _forms = {} 19 | 20 | for locale in tornado.locale.get_supported_locales(): 21 | 22 | _ = tornado.locale.get(locale).translate 23 | #logging.info('create forms: %s' % locale) 24 | 25 | is_username = regexp(USERNAME_RE, 26 | message=_("You can only use letters, numbers or dashes")) 27 | 28 | class FormWrapper(object): 29 | 30 | class LoginForm(Form): 31 | login = TextField(_("Username or email"), validators=[ 32 | required(message=\ 33 | _("You must provide an email or username"))]) 34 | 35 | password = PasswordField(_("Password")) 36 | 37 | remember = BooleanField(_("Remember me")) 38 | 39 | next = HiddenField() 40 | 41 | submit = SubmitField(_("Login")) 42 | 43 | 44 | class SignupForm(Form): 45 | username = TextField(_("Username"), validators=[ 46 | required(message=_("Username required")), 47 | is_username]) 48 | 49 | nickname = TextField(_("Nickname"), validators=[ 50 | required(message=_("Nickname required"))]) 51 | 52 | password = PasswordField(_("Password"), validators=[ 53 | required(message=_("Password required"))]) 54 | 55 | password_again = PasswordField(_("Password again"), validators=[ 56 | equal_to("password", message=\ 57 | _("Passwords don't match"))]) 58 | 59 | email = TextField(_("Email"), validators=[ 60 | required(message=_("Email required")), 61 | email(message=_("A valid email is required"))]) 62 | 63 | code = TextField(_("Signup Code")) 64 | 65 | next = HiddenField() 66 | 67 | submit = SubmitField(_("Signup")) 68 | 69 | def validate_username(self, field): 70 | user = User.query.filter(User.username.like(field.data)).first() 71 | if user: 72 | raise ValidationError, _("This username is taken") 73 | 74 | def validate_email(self, field): 75 | user = User.query.filter(User.email.like(field.data)).first() 76 | if user: 77 | raise ValidationError, _("This email is taken") 78 | 79 | 80 | class RecoverPasswordForm(Form): 81 | email = TextField(_("Your email address"), validators=[ 82 | email(message=_("A valid email is required"))]) 83 | 84 | submit = SubmitField(_("Find password")) 85 | 86 | 87 | class ChangePasswordForm(Form): 88 | password_old = PasswordField(_("Password"), validators=[ 89 | required(message=_("Password is required"))]) 90 | 91 | password = PasswordField(_("New Password"), validators=[ 92 | required(message=_("New Password is required"))]) 93 | 94 | password_again = PasswordField(_("Password again"), validators=[ 95 | equal_to("password", message=\ 96 | _("Passwords don't match"))]) 97 | 98 | submit = SubmitField(_("Save")) 99 | 100 | 101 | class DeleteAccountForm(Form): 102 | recaptcha = TextField(_("Recaptcha")) 103 | 104 | submit = SubmitField(_("Delete")) 105 | 106 | 107 | class PostForm(Form): 108 | title = TextField(_("Title"), validators=[ 109 | required(message=_("Title required"))]) 110 | 111 | slug = TextField(_("Slug")) 112 | 113 | content = TextAreaField(_("Content"), validators=[ 114 | required(message=_("Content required"))]) 115 | 116 | tags = TextField(_("Tags"), validators=[ 117 | required(message=_("Tags required"))]) 118 | 119 | submit = SubmitField(_("Save")) 120 | 121 | next = HiddenField() 122 | 123 | def validate_slug(self, field): 124 | if len(field.data) > 50: 125 | raise ValidationError, _("Slug must be less than 50 characters") 126 | slug = slugify(field.data) if field.data else slugify(self.title.data)[:50] 127 | posts = Post.query.filter_by(slug=slug) 128 | if self.obj: 129 | posts = posts.filter(db.not_(Post.id==self.obj.id)) 130 | if posts.count(): 131 | error = _("This slug is taken") if field.data else _("Slug is required") 132 | raise ValidationError, error 133 | 134 | 135 | class CommentForm(Form): 136 | email = TextField(_("Email"), validators=[ 137 | required(message=_("Email required")), 138 | email(message=_("A valid email is required"))]) 139 | 140 | nickname = TextField(_("Nickname"), validators=[ 141 | required(message=_("Nickname required"))]) 142 | 143 | website = TextField(_("Website"), validators=[ 144 | optional(), 145 | url(message=_("A valid url is required"))]) 146 | 147 | comment = TextAreaField(_("Comment"), validators=[ 148 | required(message=_("Comment required"))]) 149 | 150 | captcha = TextField(_("Captcha"), validators=[ 151 | required(message=_("Captcha required"))]) 152 | 153 | submit = SubmitField(_("Add comment")) 154 | cancel = SubmitField(_("Cancel")) 155 | 156 | 157 | class LinkForm(Form): 158 | name = TextField(_("Site name"), validators=[ 159 | required(message=_("Site name required"))]) 160 | 161 | link = TextField(_("link"), validators=[ 162 | url(message=_("A valid url is required"))]) 163 | 164 | email = TextField(_("Email"), validators=[ 165 | email(message=_("A valid email is required"))]) 166 | 167 | logo = TextField(_("Logo"), validators=[ 168 | optional(), 169 | url(message=_("A valid url is required"))]) 170 | 171 | description = TextAreaField(_("Description")) 172 | 173 | submit = SubmitField(_("Save")) 174 | 175 | 176 | _forms[locale] = FormWrapper 177 | 178 | return _forms 179 | 180 | -------------------------------------------------------------------------------- /pypress/helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | helpers.py 5 | ~~~~~~~~~~~~~ 6 | :author: laoqiu.com@gmail.com 7 | """ 8 | import re 9 | import markdown 10 | import functools 11 | import hashlib 12 | import random 13 | 14 | from pygments import highlight 15 | from pygments.lexers import get_lexer_by_name 16 | from pygments.formatters import HtmlFormatter 17 | 18 | 19 | _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+') 20 | _pre_re = re.compile(r'
(?P[\w\W]+?)
') 21 | _lang_re = re.compile(r'lang=[\'"]?(?P\w+)[\'"]?') 22 | 23 | 24 | class Storage(dict): 25 | """ 26 | A Storage object is like a dictionary except `obj.foo` can be used 27 | in addition to `obj['foo']`. 28 | >>> o = storage(a=1) 29 | >>> o.a 30 | 1 31 | >>> o['a'] 32 | 1 33 | >>> o.a = 2 34 | >>> o['a'] 35 | 2 36 | >>> del o.a 37 | >>> o.a 38 | Traceback (most recent call last): 39 | ... 40 | AttributeError: 'a' 41 | """ 42 | def __getattr__(self, key): 43 | try: 44 | return self[key] 45 | except KeyError, k: 46 | raise AttributeError, k 47 | 48 | def __setattr__(self, key, value): 49 | self[key] = value 50 | 51 | def __delattr__(self, key): 52 | try: 53 | del self[key] 54 | except KeyError, k: 55 | raise AttributeError, k 56 | 57 | def __repr__(self): 58 | return '' 59 | 60 | 61 | class Gravatar(object): 62 | """ 63 | Simple object for create gravatar link. 64 | 65 | gravatar = Gravatar( 66 | size=100, 67 | rating='g', 68 | default='retro', 69 | force_default=False, 70 | force_lower=False 71 | ) 72 | 73 | :param app: Your Flask app instance 74 | :param size: Default size for avatar 75 | :param rating: Default rating 76 | :param default: Default type for unregistred emails 77 | :param force_default: Build only default avatars 78 | :param force_lower: Make email.lower() before build link 79 | 80 | From flask-gravatar http://packages.python.org/Flask-Gravatar/ 81 | 82 | """ 83 | def __init__(self, size=100, rating='g', default='mm', 84 | force_default=False, force_lower=False): 85 | 86 | self.size = size 87 | self.rating = rating 88 | self.default = default 89 | self.force_default = force_default 90 | 91 | def __call__(self, email, size=None, rating=None, default=None, 92 | force_default=None, force_lower=False): 93 | 94 | """Build gravatar link.""" 95 | 96 | if size is None: 97 | size = self.size 98 | 99 | if rating is None: 100 | rating = self.rating 101 | 102 | if default is None: 103 | default = self.default 104 | 105 | if force_default is None: 106 | force_default = self.force_default 107 | 108 | if force_lower is None: 109 | force_lower = self.force_lower 110 | 111 | if force_lower: 112 | email = email.lower() 113 | 114 | hash = hashlib.md5(email).hexdigest() 115 | 116 | link = 'http://www.gravatar.com/avatar/{hash}'\ 117 | '?s={size}&d={default}&r={rating}'.format(**locals()) 118 | 119 | if force_default: 120 | link = link + '&f=y' 121 | 122 | return link 123 | 124 | 125 | def setting_from_object(obj): 126 | settings = dict() 127 | for key in dir(obj): 128 | if key.isupper(): 129 | settings[key.lower()] = getattr(obj, key) 130 | return settings 131 | 132 | 133 | def slugify(text, delim=u'-'): 134 | """Generates an ASCII-only slug. From http://flask.pocoo.org/snippets/5/""" 135 | result = [] 136 | for word in _punct_re.split(text.lower()): 137 | #word = word.encode('translit/long') 138 | if word: 139 | result.append(word) 140 | return unicode(delim.join(result)) 141 | 142 | 143 | def generate_random(length=8): 144 | """Generate random number.""" 145 | return ''.join([str(random.randint(0, 9)) for i in range(length)]) 146 | 147 | 148 | def endtags(html): 149 | """ close all open html tags at the end of the string """ 150 | 151 | NON_CLOSING_TAGS = ['AREA', 'BASE', 'BASEFONT', 'BR', 'COL', 'FRAME', 152 | 'HR', 'IMG', 'INPUT', 'ISINDEX', 'LINK', 'META', 'PARAM'] 153 | 154 | opened_tags = re.findall(r"<([a-z]+)[^<>]*>",html) 155 | closed_tags = re.findall(r"",html) 156 | 157 | opened_tags = [i.lower() for i in opened_tags if i.upper() not in NON_CLOSING_TAGS] 158 | closed_tags = [i.lower() for i in closed_tags] 159 | 160 | len_opened = len(opened_tags) 161 | 162 | if len_opened==len(closed_tags): 163 | return html 164 | 165 | opened_tags.reverse() 166 | 167 | for tag in opened_tags: 168 | if tag in closed_tags: 169 | closed_tags.remove(tag) 170 | else: 171 | html += "" % tag 172 | 173 | return html 174 | 175 | 176 | def gistcode(content): 177 | result = list(set(re.findall(r"(]*>\s*(https://gist.github.com/\d+)\s*)", content))) 178 | for i,link in result: 179 | content = content.replace(i, '%s ' % (i, link)) 180 | return content 181 | 182 | 183 | def code_highlight(value): 184 | f_list = _pre_re.findall(value) 185 | 186 | if f_list: 187 | s_list = _pre_re.split(value) 188 | 189 | for code_block in _pre_re.finditer(value): 190 | 191 | lang = _lang_re.search(code_block.group()).group('lang') 192 | code = code_block.group('code') 193 | 194 | index = s_list.index(code) 195 | s_list[index] = code2html(code, lang) 196 | 197 | return u''.join(s_list) 198 | 199 | return value 200 | 201 | 202 | def code2html(code, lang): 203 | lexer = get_lexer_by_name(lang, stripall=True) 204 | formatter = HtmlFormatter() 205 | return highlight(code, lexer, formatter) 206 | 207 | 208 | markdown = functools.partial(markdown.markdown, 209 | safe_mode='remove', 210 | output_format="html") 211 | 212 | storage = Storage 213 | gravatar = Gravatar() 214 | 215 | -------------------------------------------------------------------------------- /pypress/local_settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | SQLALCHEMY_DATABASE_URI = "mysql://root@localhost/pypress?charset=utf8" 5 | SQLALCHEMY_DATABASE_ECHO = False 6 | 7 | -------------------------------------------------------------------------------- /pypress/models/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from .users import * 3 | from .blog import * 4 | from .links import * 5 | -------------------------------------------------------------------------------- /pypress/models/links.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | models: links.py 5 | ~~~~~~~~~~~~~ 6 | :author: laoqiu.com@gmail.com 7 | """ 8 | from datetime import datetime 9 | 10 | from pypress.extensions.cache import cached_property 11 | from pypress.helpers import storage 12 | from pypress.database import db 13 | 14 | 15 | __all__ = ['Link',] 16 | 17 | class Link(db.Model): 18 | 19 | __tablename__ = "links" 20 | 21 | PER_PAGE = 80 22 | 23 | id = db.Column(db.Integer, primary_key=True) 24 | name = db.Column(db.Unicode(50), nullable=False) 25 | link = db.Column(db.String(100), nullable=False) 26 | logo = db.Column(db.String(100)) 27 | description = db.Column(db.Unicode(100)) 28 | email = db.Column(db.String(50)) 29 | passed = db.Column(db.Boolean, default=False) 30 | created_date = db.Column(db.DateTime, default=datetime.utcnow) 31 | 32 | class Permissions(object): 33 | 34 | def __init__(self, obj): 35 | self.obj = obj 36 | 37 | 38 | def __init__(self, *args, **kwargs): 39 | super(Link, self).__init__(*args, **kwargs) 40 | 41 | def __str__(self): 42 | return self.name 43 | 44 | def __repr__(self): 45 | return "<%s>" % self 46 | 47 | @cached_property 48 | def permissions(self): 49 | return self.Permissions(self) 50 | 51 | @cached_property 52 | def json(self): 53 | return dict(id=self.id, 54 | name=self.name, 55 | url=self.link, 56 | logo=self.logo, 57 | description=self.description, 58 | created_date=self.created_date) 59 | 60 | @cached_property 61 | def item(self): 62 | return storage(self.json) 63 | -------------------------------------------------------------------------------- /pypress/models/types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | types.py 5 | ~~~~~~~~~~~~~ 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | from sqlalchemy import types 10 | 11 | class DenormalizedText(types.MutableType, types.TypeDecorator): 12 | """ 13 | Stores denormalized primary keys that can be 14 | accessed as a set. 15 | 16 | :param coerce: coercion function that ensures correct 17 | type is returned 18 | 19 | :param separator: separator character 20 | """ 21 | 22 | impl = types.Text 23 | 24 | def __init__(self, coerce=int, separator=" ", **kwargs): 25 | 26 | self.coerce = coerce 27 | self.separator = separator 28 | 29 | super(DenormalizedText, self).__init__(**kwargs) 30 | 31 | def process_bind_param(self, value, dialect): 32 | if value is not None: 33 | items = [str(item).strip() for item in value] 34 | value = self.separator.join(item for item in items if item) 35 | return value 36 | 37 | def process_result_value(self, value, dialect): 38 | if not value: 39 | return set() 40 | return set(self.coerce(item) \ 41 | for item in value.split(self.separator)) 42 | 43 | def copy_value(self, value): 44 | return set(value) 45 | 46 | -------------------------------------------------------------------------------- /pypress/models/users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | models: users.py 5 | ~~~~~~~~~~~~~ 6 | :author: laoqiu.com@gmail.com 7 | """ 8 | 9 | import hashlib 10 | from datetime import datetime 11 | 12 | import tornado.web 13 | 14 | from pypress.extensions.sqlalchemy import BaseQuery 15 | from pypress.extensions.permission import Permission, RoleNeed, UserNeed 16 | from pypress.extensions.cache import cached_property 17 | from pypress.permissions import admin 18 | from pypress.database import db 19 | 20 | 21 | __all__ = ['User', 'UserCode', 'Tweet', ] 22 | 23 | 24 | class UserQuery(BaseQuery): 25 | 26 | def authenticate(self, login, password): 27 | 28 | user = self.filter(db.or_(User.username==login, 29 | User.email==login)).first() 30 | 31 | if user: 32 | authenticated = user.check_password(password) 33 | else: 34 | authenticated = False 35 | 36 | return user, authenticated 37 | 38 | def search(self, key): 39 | query = self.filter(db.or_(User.email==key, 40 | User.nickname.ilike('%'+key+'%'), 41 | User.username.ilike('%'+key+'%'))) 42 | return query 43 | 44 | def get_by_username(self, username): 45 | user = self.filter(User.username==username).first() 46 | if user is None: 47 | raise tornado.web.HTTPError(404) 48 | return user 49 | 50 | 51 | class User(db.Model): 52 | 53 | __tablename__ = 'users' 54 | 55 | query_class = UserQuery 56 | 57 | PER_PAGE = 50 58 | TWEET_PER_PAGE = 30 59 | 60 | MEMBER = 100 61 | MODERATOR = 200 62 | ADMIN = 300 63 | 64 | id = db.Column(db.Integer, primary_key=True) 65 | username = db.Column(db.String(20), unique=True) 66 | nickname = db.Column(db.String(20)) 67 | email = db.Column(db.String(100), unique=True, nullable=False) 68 | _password = db.Column("password", db.String(80), nullable=False) 69 | role = db.Column(db.Integer, default=MEMBER) 70 | activation_key = db.Column(db.String(40)) 71 | date_joined = db.Column(db.DateTime, default=datetime.utcnow) 72 | last_login = db.Column(db.DateTime, default=datetime.utcnow) 73 | block = db.Column(db.Boolean, default=False) 74 | 75 | class Permissions(object): 76 | 77 | def __init__(self, obj): 78 | self.obj = obj 79 | 80 | @cached_property 81 | def edit(self): 82 | return Permission(UserNeed(self.obj.id)) & admin 83 | 84 | 85 | def __init__(self, *args, **kwargs): 86 | super(User, self).__init__(*args, **kwargs) 87 | 88 | def __str__(self): 89 | return "User: %s" % self.nickname 90 | 91 | def __repr__(self): 92 | return "<%s>" % self 93 | 94 | @cached_property 95 | def permissions(self): 96 | return self.Permissions(self) 97 | 98 | @cached_property 99 | def provides(self): 100 | needs = [RoleNeed('authenticated'), 101 | UserNeed(self.id)] 102 | 103 | if self.is_moderator: 104 | needs.append(RoleNeed('moderator')) 105 | 106 | if self.is_admin: 107 | needs.append(RoleNeed('admin')) 108 | 109 | return needs 110 | 111 | def _get_password(self): 112 | return self._password 113 | 114 | def _set_password(self, password): 115 | self._password = hashlib.md5(password).hexdigest() 116 | 117 | password = db.synonym("_password", 118 | descriptor=property(_get_password, 119 | _set_password)) 120 | 121 | def check_password(self,password): 122 | if self.password is None: 123 | return False 124 | return self.password == hashlib.md5(password).hexdigest() 125 | 126 | @property 127 | def is_moderator(self): 128 | return self.role >= self.MODERATOR 129 | 130 | @property 131 | def is_admin(self): 132 | return self.role >= self.ADMIN 133 | 134 | @property 135 | def json(self): 136 | return dict(id=self.id, 137 | username=self.username, 138 | nickname=self.nickname, 139 | email=self.email, 140 | is_admin=self.is_admin, 141 | is_moderator=self.is_moderator, 142 | last_login=self.last_login) 143 | 144 | 145 | class UserCode(db.Model): 146 | 147 | __tablename__ = 'usercode' 148 | 149 | id = db.Column(db.Integer, primary_key=True) 150 | code = db.Column(db.String(20), nullable=False) 151 | role = db.Column(db.Integer, default=User.MEMBER) 152 | 153 | def __init__(self, *args, **kwargs): 154 | super(UserCode, self).__init__(*args, **kwargs) 155 | 156 | def __str__(self): 157 | return self.code 158 | 159 | def __repr__(self): 160 | return "<%s>" % self 161 | 162 | 163 | class Tweet(db.Model): 164 | 165 | __tablename__ = 'tweets' 166 | 167 | id = db.Column(db.Integer, primary_key=True) 168 | 169 | user_id = db.Column(db.Integer, 170 | db.ForeignKey(User.id, ondelete='CASCADE'), 171 | nullable=False, 172 | unique=True) 173 | 174 | server = db.Column(db.String(50)) 175 | token = db.Column(db.String(50)) 176 | token_secret = db.Column(db.String(50)) 177 | 178 | def __init__(self, *args, **kwargs): 179 | super(Tweet, self).__init__(*args, **kwargs) 180 | 181 | def __str__(self): 182 | return "Tweet: %s" % self.id 183 | 184 | def __repr__(self): 185 | return "<%s>" % self 186 | 187 | 188 | User.tweets = db.relation(Tweet, backref="user") 189 | 190 | # to do ... 191 | User.weibo = None 192 | User.douban = None 193 | User.qq = None 194 | 195 | -------------------------------------------------------------------------------- /pypress/permissions.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | permissions.py 5 | ~~~~~~~~~~~ 6 | set role need permissions 7 | :author: laoqiu.com@gmail.com 8 | """ 9 | 10 | from extensions.permission import RoleNeed, Permission 11 | 12 | admin = Permission(RoleNeed('admin')) 13 | moderator = Permission(RoleNeed('moderator')) 14 | auth = Permission(RoleNeed('authenticated')) 15 | 16 | -------------------------------------------------------------------------------- /pypress/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | import os 4 | 5 | DEBUG = True 6 | COOKIE_SECRET = 'simple' 7 | LOGIN_URL = '/login' 8 | XSRF_COOKIES = True 9 | THEME_NAME = 'simple' 10 | 11 | # default templates and static path settings 12 | TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), 'templates') 13 | STATIC_PATH = os.path.join(os.path.dirname(__file__), 'static') 14 | 15 | # theme path settings 16 | THEME_PATH = os.path.join(os.path.dirname(__file__), 'themes', THEME_NAME) 17 | THEME_TEMPLATE_PATH = os.path.join(THEME_PATH, 'templates') 18 | THEME_STATIC_PATH = os.path.join(THEME_PATH, 'static') 19 | 20 | UPLOAD_PATH = os.path.join(os.path.dirname(__file__), 'uploads') 21 | 22 | DEFAULT_LOCALE = 'en_US' #'zh_CN' 23 | 24 | # REDIS_SERVER = False 25 | 26 | # If set to None or 0 the session will be deleted when the user closes the browser. 27 | # If set number the session lives for value days. 28 | PERMANENT_SESSION_LIFETIME = 1 # days 29 | 30 | # redis connection config 31 | REDIS_HOST = 'localhost' 32 | REDIS_PORT = 6379 33 | REDIS_DB = 0 34 | 35 | try: 36 | from local_settings import * 37 | except: 38 | pass 39 | 40 | -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/b.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/bg_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/bg_button.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/bgcolor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/bgcolor.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/close.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/code.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/color.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/del.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/del.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/i.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/i.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/image.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/indent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/indent.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/justifycenter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/justifycenter.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/justifyleft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/justifyleft.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/justifyright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/justifyright.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/link.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/ol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/ol.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/outdent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/outdent.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/pagebreaks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/pagebreaks.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/source.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/strikethrough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/strikethrough.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/titlebg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/titlebg.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/u.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/ul.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/ul.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/img/underline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/static/js/uEditor/img/underline.png -------------------------------------------------------------------------------- /pypress/static/js/uEditor/uEditor.css: -------------------------------------------------------------------------------- 1 | .uEditor { 2 | float: left; 3 | width: 100%; 4 | height: auto; 5 | padding: 0; 6 | margin: 10px 0; 7 | border: 1px solid #CED5E0; 8 | overflow: hidden; 9 | } 10 | .uEditor .uEditorIframe, 11 | .uEditor textarea.uEditorTextarea 12 | { 13 | height: 360px; 14 | } 15 | .uEditor ul.uEditorToolbar { 16 | background: #F8FAFD; 17 | border-bottom:1px solid #CED5E0; 18 | list-style: none; 19 | height: 22px; 20 | margin: 0; 21 | padding:5px; 22 | margin:0; 23 | } 24 | .uEditor ul.uEditorToolbar li { 25 | display:inline; 26 | margin: 0 5px 0 0; 27 | float:left; 28 | cursor:pointer; 29 | } 30 | .uEditor ul.uEditorToolbar li a { 31 | color:#999; 32 | display:inline; 33 | margin: 0; 34 | padding: 2px; 35 | float:left; 36 | cursor:pointer; 37 | text-indent:-1000px; 38 | width:16px; 39 | height:16px; 40 | -moz-border-radius: 2px; 41 | -webkit-border-radius: 2px; 42 | border-radius: 2px; 43 | border: 1px solid transparent; 44 | background-repeat: no-repeat; 45 | background-position: 50% 50%; 46 | } 47 | .uEditor ul.uEditorToolbar li a.on { 48 | border-color: #729BD1; 49 | background-color: #DDE1EB; 50 | } 51 | .uEditor ul.uEditorToolbar li a.off { 52 | cursor: default; 53 | opacity: 0.3; 54 | } 55 | 56 | .uEditor ul.uEditorToolbar li a.uEditorButtonBold { 57 | background-image: url('img/b.png'); 58 | } 59 | .uEditor ul.uEditorToolbar li a.uEditorButtonItalic { 60 | background-image: url('img/i.png'); 61 | } 62 | .uEditor ul.uEditorToolbar li a.uEditorButtonUnderline { 63 | background-image: url('img/underline.png'); 64 | } 65 | .uEditor ul.uEditorToolbar li a.uEditorButtonOrderedList { 66 | background-image: url('img/ol.png'); 67 | } 68 | .uEditor ul.uEditorToolbar li a.uEditorButtonUnorderedList { 69 | background-image: url('img/ul.png'); 70 | } 71 | .uEditor ul.uEditorToolbar li a.uEditorButtonHyperlink { 72 | background-image: url('img/link.png'); 73 | } 74 | .uEditor ul.uEditorToolbar li a.uEditorButtonImage { 75 | background-image: url('img/image.png'); 76 | } 77 | .uEditor ul.uEditorToolbar li a.uEditorButtonHTML { 78 | background-image: url('img/source.png'); 79 | } 80 | .uEditor ul.uEditorToolbar li a.uEditorButtonJustifyLeft { 81 | background-image: url('img/justifyleft.png'); 82 | } 83 | .uEditor ul.uEditorToolbar li a.uEditorButtonJustifyCenter { 84 | background-image: url('img/justifycenter.png'); 85 | } 86 | .uEditor ul.uEditorToolbar li a.uEditorButtonJustifyRight { 87 | background-image: url('img/justifyright.png'); 88 | } 89 | .uEditor ul.uEditorToolbar li a.uEditorButtonIndent { 90 | background-image: url('img/indent.png'); 91 | } 92 | .uEditor ul.uEditorToolbar li a.uEditorButtonOutdent { 93 | background-image: url('img/outdent.png'); 94 | } 95 | .uEditor ul.uEditorToolbar li a.uEditorButtonColor { 96 | width: 24px; 97 | background-image: url('img/color.png'); 98 | } 99 | .uEditor ul.uEditorToolbar li a.uEditorButtonBgColor { 100 | width: 24px; 101 | background-image: url('img/bgcolor.png'); 102 | } 103 | .uEditor ul.uEditorToolbar li a.uEditorButtonStrikeThrough { 104 | background-image: url('img/strikethrough.png'); 105 | } 106 | .uEditor ul.uEditorToolbar li a.uEditorButtonPagebreaks { 107 | background-image: url('img/pagebreaks.png'); 108 | } 109 | .uEditor ul.uEditorToolbar li a.uEditorButtonCode { 110 | background-image: url('img/code.png'); 111 | } 112 | .uEditor ul.uEditorToolbar li a.myButtonClass, 113 | .uEditor ul.uEditorToolbar li a.mySecondButtonClass { 114 | background: url(img/plugin.png); 115 | } 116 | .uEditor ul.uEditorToolbar li.uEditorEditSelect { 117 | margin:0; 118 | padding:0; 119 | } 120 | .uEditor ul.uEditorToolbar li a:hover { 121 | border-color: #DDE1EB; 122 | } 123 | .uEditor ul.uEditorToolbar li a.on:hover { 124 | border-color: #A1BADF; 125 | } 126 | .uEditor .uEditorIframe { 127 | clear: both; 128 | margin: 0; 129 | padding: 0; 130 | width: 100%; 131 | border: 0; 132 | } 133 | .uEditor textarea.uEditorTextarea 134 | { 135 | clear: both; 136 | width: 100%; 137 | border: none; 138 | font-family:monospace; 139 | } 140 | .uEditor-button { 141 | cursor: pointer; 142 | padding: 2px 10px; 143 | border: 1px solid #ccc; 144 | border-color: #aaa #aaa #666; 145 | -moz-border-radius: 5px; 146 | -khtml-border-radius: 5px; 147 | -webkit-border-radius: 5px; 148 | border-radius: 5px; 149 | -webkit-box-shadow:0 1px 3px #ddd; 150 | color: #555; 151 | background: #f5f5f5 url("img/bg_button.png"); 152 | font-size: 14px; 153 | font-weight: bold; 154 | } 155 | .uEditor-overlay { 156 | position: absolute; 157 | z-index: 90; 158 | left: 0; 159 | top: 0; 160 | width: 100%; 161 | background-color: gray; 162 | } 163 | .uEditor-dialog { 164 | position: absolute; 165 | background-color: #FBFBFB; 166 | border: 1px solid #ACB4BE; 167 | font-size: 12px; 168 | } 169 | .uEditor-dialog h3 { 170 | padding: 0 0 0 18px; 171 | height: 30px; 172 | line-height: 30px; 173 | background: #D6DBE2 url('img/titlebg.png') repeat-x 0 0; 174 | font-size: 14px; 175 | font-weight: bold; 176 | border-bottom: 1px solid #ACB4BE; 177 | } 178 | .uEditor-upload { 179 | z-index: 99; 180 | } 181 | .uEditor-upload h3, 182 | .uEditor-upload p { 183 | margin: 0; 184 | } 185 | .uEditor-upload-content { 186 | width: 500px; 187 | background: #fff; 188 | } 189 | .uEditor-upload-content-body { 190 | padding: 5px 30px 25px; 191 | } 192 | .uEditor-upload-content-body p { 193 | padding: 5px 0; 194 | } 195 | .uEditor-upload-content-body p label { 196 | margin-right: 15px; 197 | } 198 | .uEditor-upload-content-body p .uEditor-button { 199 | margin-left: 62px; 200 | } 201 | .uEditor-tabs ul { 202 | border-bottom: 1px solid #CED5E0; 203 | margin-top: 15px; 204 | padding-left: 10px; 205 | } 206 | .uEditor-tabs ul li { 207 | float: left; 208 | list-style: none; 209 | cursor: pointer; 210 | position: relative; 211 | margin: 0 10px; 212 | width: 120px; 213 | height: 24px; 214 | line-height: 24px; 215 | background-color: #F2F2F2; 216 | border-color: #CED5E0; 217 | border-style: solid solid none; 218 | border-width: 1px 1px medium; 219 | text-align: center; 220 | } 221 | .uEditor-tabs ul li.selected { 222 | cursor: default; 223 | background-color: #FCFCFC; 224 | border-color: #CED5E0 #CED5E0 #FCFCFC; 225 | margin-bottom: -1px; 226 | padding-bottom: 1px; 227 | } 228 | .uEditor-input { 229 | padding: 2px 5px; 230 | } 231 | .uEditor-color-panel a { 232 | color: black; 233 | display: block; 234 | text-decoration: none; 235 | } 236 | .uEditor-color-panel a:hover { 237 | color: black; 238 | text-decoration: none; 239 | } 240 | .uEditor-color-panel a:active { 241 | color: black; 242 | } 243 | .uEditor-color-palette { 244 | margin: 5px 8px 8px; 245 | } 246 | .uEditor-color-palette table { 247 | border: 1px solid #666666; 248 | border-collapse: collapse; 249 | border-spacing: 0; 250 | } 251 | .uEditor-color-palette td { 252 | margin: 0; 253 | padding: 0; 254 | border-right: 1px solid #666666; 255 | height: 18px; 256 | width: 18px; 257 | } 258 | a.uEditor-color-a { 259 | height: 18px; 260 | width: 18px; 261 | } 262 | a.uEditor-color-a:hover { 263 | border: 1px solid #FFFFFF; 264 | height: 16px; 265 | width: 16px; 266 | } 267 | a.uEditor-color-remove { 268 | margin: 2px 0 3px; 269 | padding: 3px 8px; 270 | font-size: 12px; 271 | } 272 | a.uEditor-color-remove:hover { 273 | background-color: #D6E9F8; 274 | } 275 | .uEditor-clear:after { 276 | clear: both; 277 | content: " "; 278 | display: block; 279 | height: 0; 280 | } 281 | .uEditor-clear:after { 282 | clear: both; 283 | content: " "; 284 | display: block; 285 | height: 0; 286 | } 287 | 288 | #uEditor-imageurl { 289 | width: 300px; 290 | } 291 | #uEditor-imagealt { 292 | width: 100px; 293 | } 294 | -------------------------------------------------------------------------------- /pypress/static/js/uEditor/uEditorContent.css: -------------------------------------------------------------------------------- 1 | /* HTML TAGS */ 2 | html,body,h1,h2,h3,h4,h5,h6,td,th,dd,p,div,li { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | padding: 10px; 9 | font: 1em/1.6em 'Georgia',Verdana,Arial,sans-serif; 10 | color: #333; 11 | background: #fff; 12 | } 13 | 14 | h1 { 15 | font-size: 20px; 16 | line-height: 2em; 17 | } 18 | h2 { 19 | font-size: 18px; 20 | line-height: 2em; 21 | } 22 | h3 { 23 | font-size: 14px; 24 | line-height: 1.6em; 25 | } 26 | h4,h5,h6 { 27 | font-size: 12px; 28 | line-height: 1.5em; 29 | } 30 | 31 | input, textarea { 32 | font-size: 14px; 33 | } 34 | 35 | a:link, a:visited { 36 | color: #06c; 37 | text-decoration: none; 38 | } 39 | a:hover { 40 | color: #666; 41 | } 42 | 43 | a img { 44 | border: 0; 45 | } 46 | 47 | blockquote { 48 | color: #555; 49 | font-style: italic; 50 | font-size: 17px; 51 | } 52 | 53 | p { 54 | margin: 10px 0; 55 | font-size: 14px; 56 | line-height: 1.6em; 57 | } 58 | 59 | pre { 60 | display: block; 61 | padding: 15px; 62 | font-size: 0.75em; 63 | border-color: #B3B3B3 #B3B3B3 #B3B3B3 #DDDDDD; 64 | border-style: dotted dotted dotted solid; 65 | border-width: 1px 1px 1px 5px; 66 | } 67 | -------------------------------------------------------------------------------- /pypress/themes/default/static/base.css: -------------------------------------------------------------------------------- 1 | html,body,h1,h2,h3,h4,h5,h6,td,th,dd,p,div,ol,ul,li { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | font: 0.75em/1.5em 'Georgia','Verdana','Arial',sans-serif; 8 | color: #333; 9 | background: #fff; 10 | } 11 | 12 | /***** public class settings ****/ 13 | 14 | h1 { 15 | font-size: 20px; 16 | line-height: 2em; 17 | } 18 | h2 { 19 | font-size: 18px; 20 | line-height: 2em; 21 | } 22 | h3 { 23 | font-size: 14px; 24 | line-height: 1.6em; 25 | } 26 | h4,h5,h6 { 27 | font-size: 12px; 28 | line-height: 1.5em; 29 | } 30 | 31 | input, textarea { 32 | font-size: 14px; 33 | } 34 | 35 | a:link, a:visited { 36 | color: #06c; 37 | text-decoration: none; 38 | } 39 | a:hover { 40 | color: #666; 41 | } 42 | 43 | a img { 44 | border: 0; 45 | } 46 | 47 | blockquote { 48 | color: #555; 49 | font-style: italic; 50 | font-size: 17px; 51 | } 52 | 53 | .clearfix:before,.clearfix:after { 54 | content:"."; 55 | display:block; 56 | height:0; 57 | overflow:hidden; 58 | } 59 | 60 | .clearfix:after, .clear { 61 | clear:both; 62 | } 63 | 64 | .clearfix{ 65 | zoom:1; /* IE < 8 */ 66 | } 67 | 68 | .error-no-data { 69 | padding: 20px 0; 70 | font-size: 16px; 71 | line-height: 1.6em; 72 | } 73 | .errors { 74 | clear: both; 75 | color: #e00; 76 | } 77 | 78 | #flashed { 79 | padding: 5px; 80 | color: #f60; 81 | background: #eeefce; 82 | text-align: center; 83 | } 84 | 85 | #go-to-top { 86 | display: block; 87 | position: fixed; 88 | right: 60px; 89 | bottom: 100px; 90 | padding: 18px; 91 | color: #666; 92 | background: #ddd; 93 | font: 36px/18px Helvetica,Arial,Verdana,sans-serif; 94 | opacity: 0.7; 95 | outline: 0 none; 96 | text-decoration: none; 97 | text-shadow: 0 0 1px #ddd; 98 | vertical-align: baseline; 99 | -moz-border-radius: 5px; 100 | -khtml-border-radius: 5px; 101 | -webkit-border-radius: 5px; 102 | border-radius: 5px; 103 | } 104 | 105 | 106 | #wrap { 107 | margin: auto; 108 | } 109 | 110 | #header { 111 | height: 60px; 112 | color: #eee; 113 | background: #546f8c; 114 | } 115 | #header h1 { 116 | padding-top: 10px; 117 | } 118 | 119 | #header .inner, #container .inner, #footer .inner { 120 | margin: auto; 121 | width: 960px; 122 | } 123 | 124 | #header .inner { 125 | position: relative; 126 | } 127 | 128 | #nav { 129 | position: absolute; 130 | top: 10px; 131 | right: 10px; 132 | width: 500px; 133 | font-size: 1.2em; 134 | } 135 | 136 | #nav li { 137 | display: block; 138 | float: left; 139 | padding: 0 10px; 140 | } 141 | #nav li a { 142 | color: #fff; 143 | } 144 | 145 | #nav li.current_user { 146 | float: right; 147 | } 148 | 149 | 150 | 151 | #container { 152 | padding: 32px 0; 153 | background: #ececec url('bg.gif') repeat 0 0; 154 | } 155 | 156 | #post-form input.text { 157 | width: 500px; 158 | } 159 | 160 | #post-form textarea.text { 161 | width: 600px; 162 | height: 300px; 163 | } 164 | 165 | /***** form *****/ 166 | .form { 167 | padding: 24px 36px; 168 | background: #fff; 169 | -moz-border-radius: 5px; 170 | -khtml-border-radius: 5px; 171 | -webkit-border-radius: 5px; 172 | border-radius: 5px; 173 | -webkit-box-shadow:0 0 5px #ccc; 174 | -moz-box-shadow:0 0 5px #ccc; 175 | } 176 | .form h2 { 177 | margin-bottom: 12px; 178 | } 179 | .form-field { 180 | clear: both; 181 | padding: 5px 0; 182 | } 183 | .form-field label { 184 | float: left; 185 | width: 120px; 186 | } 187 | .form-field .text { 188 | padding: 2px; 189 | width: 200px; 190 | } 191 | .form-button { 192 | cursor: pointer; 193 | background: #EA4C89 url("glass-light.png") repeat-x scroll 0 50%; 194 | border: medium none; 195 | -moz-border-radius: 6px; 196 | -khtml-border-radius: 6px; 197 | -webkit-border-radius: 6px; 198 | border-radius: 6px; 199 | color: #FFFFFF; 200 | font-weight: bold; 201 | padding: 6px 24px; 202 | text-decoration: none; 203 | } 204 | .form-button:hover, .form-button:focus { 205 | background-color: #DF3E7B; 206 | text-decoration: none; 207 | } 208 | 209 | .form-section { 210 | margin: 10px 0; 211 | } 212 | 213 | .form-section label { 214 | display: block; 215 | font-size: 14px; 216 | line-height: 1.6em; 217 | } 218 | 219 | .form-section input.text { 220 | width: 70%; 221 | } 222 | 223 | .form-section textarea { 224 | width: 100%; 225 | height: 300px; 226 | } 227 | 228 | .form-section input.submit { 229 | font-size: 16px; 230 | line-height: 1.6em; 231 | } 232 | 233 | #login-box .text { 234 | height: 16px; 235 | line-height: 16px; 236 | } 237 | 238 | #login-box .form-box, #signup-box .form-box { 239 | float: left; 240 | width: 500px; 241 | border-right: 1px solid #ddd; 242 | } 243 | 244 | #login-box .form-side, #signup-box .form-side { 245 | float: left; 246 | padding-left: 32px; 247 | width: 350px; 248 | } 249 | 250 | #login-box .form-button, 251 | #signup-box .form-button { 252 | background-color: #4D90FE; 253 | } 254 | 255 | .form .form-button, 256 | .form .errors { 257 | margin-left: 120px; 258 | } 259 | 260 | .post { 261 | padding: 10px 0; 262 | background: url("post-line.png") repeat-x 0 bottom; 263 | } 264 | 265 | .post a:link, .post a:visited { 266 | color: #597A9C; 267 | text-decoration: none; 268 | } 269 | .post a:hover { 270 | text-decoration: underline; 271 | } 272 | 273 | .post-avatar a { 274 | display: block; 275 | } 276 | 277 | .post-content { 278 | margin-bottom: 10px; 279 | padding: 10px 0; 280 | font-size: 14px; 281 | line-height: 1.6em; 282 | } 283 | .post-content hr { 284 | display: none; 285 | } 286 | .post-content p { 287 | margin: 10px auto; 288 | } 289 | .post-content pre { 290 | display: block; 291 | padding: 15px; 292 | border-color: #B3B3B3 #B3B3B3 #B3B3B3 #DDDDDD; 293 | border-style: dotted dotted dotted solid; 294 | border-width: 1px 1px 1px 5px; 295 | background: #fff; 296 | font-size: 0.9em; 297 | } 298 | .post-content ol, .post-content ul { 299 | margin: 20px 30px; 300 | } 301 | 302 | .post-meta { 303 | padding: 5px 0; 304 | color: #999; 305 | } 306 | .post-meta span { 307 | margin-right: 10px; 308 | text-transform: capitalize; 309 | } 310 | .post-foot { 311 | margin-bottom: 10px; 312 | padding: 5px 10px; 313 | height: 20px; 314 | line-height: 20px; 315 | background: #ddd; 316 | } 317 | .post-foot .prev { 318 | float: left; 319 | } 320 | .post-foot .next { 321 | float: right; 322 | } 323 | .comment-add h3, 324 | .comment-list h3 { 325 | padding: 10px 0; 326 | } 327 | .comment-add p { 328 | margin-bottom: 10px; 329 | } 330 | .comment-add p label { 331 | display: block; 332 | line-height: 24px; 333 | } 334 | .comment-add p a img { 335 | vertical-align: -6px; 336 | } 337 | .comment-add textarea { 338 | margin: 5px 0 0; 339 | padding: 3px 4px; 340 | width: 492px; 341 | height: 106px; 342 | font-size: 13px; 343 | line-height: 17px; 344 | } 345 | .comment-add input.text { 346 | width: 300px; 347 | } 348 | 349 | .comment-list ol { 350 | list-style: none; 351 | padding: 0; 352 | } 353 | .comment-list ol ol { 354 | margin-left: 70px; 355 | } 356 | .comment { 357 | margin: 15px 0; 358 | padding-top: 15px; 359 | width: 100%; 360 | border-top: 1px solid #ddd; 361 | } 362 | .comment-avatar { 363 | float: left; 364 | height: 60px; 365 | margin: 0 10px 0 0; 366 | overflow: hidden; 367 | position: relative; 368 | width: 60px; 369 | border: 1px solid #ddd; 370 | } 371 | .comment-avatar a { 372 | float: left; 373 | display: block; 374 | padding: 5px; 375 | } 376 | .comment-text { 377 | margin: 0 0 0 72px; 378 | } 379 | 380 | #tag-cloud ul { 381 | padding: 0; 382 | } 383 | #tag-cloud li { 384 | display: inline; 385 | padding: 0 5px; 386 | } 387 | a.tag-0 { color:#aaa; font-size: 0.9em; font-weight: 100; } 388 | a.tag-1 { color:#999; font-size: 0.9em; font-weight: 100; } 389 | a.tag-2 { color:#999; font-size: 1em; font-weight: 200; } 390 | a.tag-3 { color:#888; font-size: 1.1em; font-weight: 300; } 391 | a.tag-4 { color:#777; font-size: 1.2em; font-weight: 400; } 392 | a.tag-5 { color:#666; font-size: 1.3em; font-weight: 500; } 393 | a.tag-6 { color:#555; font-size: 1.4em; font-weight: 600; } 394 | a.tag-7 { color:#333; font-size: 1.6em; font-weight: 700; } 395 | a.tag-8 { color:#000; font-size: 1.8em; font-weight: 800; } 396 | a.tag-9 { color:#06c; font-size: 2em; font-weight: 900; } 397 | a.tag-10 { color:#c60; font-size: 2em; font-weight: 900; } 398 | 399 | .archives h3 { 400 | margin: 10px 0; 401 | border-bottom: 1px solid #ccc; 402 | font-size: 1.8em; 403 | line-height: 2em; 404 | font-weight: normal; 405 | } 406 | .archives ul li { 407 | list-style-type: square; 408 | line-height: 1.8em; 409 | font-size: 13px; 410 | } 411 | .archives ul li span { 412 | float: right; 413 | color: #999; 414 | } 415 | 416 | /***** footer *****/ 417 | #footer { 418 | padding: 10px; 419 | color: #999; 420 | background: #fff; 421 | border-top: 1px solid #ddd; 422 | } 423 | 424 | #footer p span { 425 | float: right; 426 | } 427 | div.uEditor { 428 | width: 650px; 429 | } 430 | 431 | -------------------------------------------------------------------------------- /pypress/themes/default/static/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/themes/default/static/bg.gif -------------------------------------------------------------------------------- /pypress/themes/default/static/blog.js: -------------------------------------------------------------------------------- 1 | var get_cookie = function(name) { 2 | var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); 3 | return r ? r[1] : undefined; 4 | } 5 | 6 | var message = function(message, category){ 7 | $('#flashed').append('' + message + ''); 8 | } 9 | 10 | var ajax_post = function(url, params, on_success){ 11 | var _callback = function(response){ 12 | if (response.success) { 13 | if (response.redirect_url){ 14 | window.location.href = response.redirect_url; 15 | } else if (response.reload){ 16 | window.location.reload(); 17 | } else if (on_success) { 18 | return on_success(response); 19 | } 20 | } else { 21 | return message(response.error, "error"); 22 | } 23 | } 24 | $.post(url, params, _callback, "json"); 25 | } 26 | 27 | var delete_comment = function(url) { 28 | var callback = function(response){ 29 | $('#comment-' + response.comment_id).remove(); 30 | } 31 | var params = {'_xsrf': get_cookie("_xsrf")}; 32 | ajax_post(url, params, callback); 33 | } 34 | 35 | var get_captcha = function(url) { 36 | var uri = url + '?t=' + String(Math.random()).slice(3,8); 37 | $("#captcha-image").attr('src', uri); 38 | } 39 | -------------------------------------------------------------------------------- /pypress/themes/default/static/glass-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/themes/default/static/glass-light.png -------------------------------------------------------------------------------- /pypress/themes/default/static/highlight.css: -------------------------------------------------------------------------------- 1 | div.highlight { margin: 0px; overflow: auto; width: 100%; } 2 | div.highlight .hll { background-color: #ffffcc } 3 | div.highlight .c { color: #8f5902; font-style: italic } /* Comment */ 4 | div.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ 5 | div.highlight .g { color: #000000 } /* Generic */ 6 | div.highlight .k { color: #204a87; font-weight: bold } /* Keyword */ 7 | div.highlight .l { color: #000000 } /* Literal */ 8 | div.highlight .n { color: #000000 } /* Name */ 9 | div.highlight .o { color: #ce5c00; font-weight: bold } /* Operator */ 10 | div.highlight .x { color: #000000 } /* Other */ 11 | div.highlight .p { color: #000000; font-weight: bold } /* Punctuation */ 12 | div.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ 13 | div.highlight .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */ 14 | div.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ 15 | div.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ 16 | div.highlight .gd { color: #a40000 } /* Generic.Deleted */ 17 | div.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ 18 | div.highlight .gr { color: #ef2929 } /* Generic.Error */ 19 | div.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 20 | div.highlight .gi { color: #00A000 } /* Generic.Inserted */ 21 | div.highlight .go { color: #000000; font-style: italic } /* Generic.Output */ 22 | div.highlight .gp { color: #8f5902 } /* Generic.Prompt */ 23 | div.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ 24 | div.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 25 | div.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ 26 | div.highlight .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */ 27 | div.highlight .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */ 28 | div.highlight .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */ 29 | div.highlight .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */ 30 | div.highlight .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */ 31 | div.highlight .kt { color: #204a87; font-weight: bold } /* Keyword.Type */ 32 | div.highlight .ld { color: #000000 } /* Literal.Date */ 33 | div.highlight .m { color: #0000cf; font-weight: bold } /* Literal.Number */ 34 | div.highlight .s { color: #4e9a06 } /* Literal.String */ 35 | div.highlight .na { color: #c4a000 } /* Name.Attribute */ 36 | div.highlight .nb { color: #204a87 } /* Name.Builtin */ 37 | div.highlight .nc { color: #000000 } /* Name.Class */ 38 | div.highlight .no { color: #000000 } /* Name.Constant */ 39 | div.highlight .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */ 40 | div.highlight .ni { color: #ce5c00 } /* Name.Entity */ 41 | div.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ 42 | div.highlight .nf { color: #000000 } /* Name.Function */ 43 | div.highlight .nl { color: #f57900 } /* Name.Label */ 44 | div.highlight .nn { color: #000000 } /* Name.Namespace */ 45 | div.highlight .nx { color: #000000 } /* Name.Other */ 46 | div.highlight .py { color: #000000 } /* Name.Property */ 47 | div.highlight .nt { color: #204a87; font-weight: bold } /* Name.Tag */ 48 | div.highlight .nv { color: #000000 } /* Name.Variable */ 49 | div.highlight .ow { color: #204a87; font-weight: bold } /* Operator.Word */ 50 | div.highlight .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ 51 | div.highlight .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */ 52 | div.highlight .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */ 53 | div.highlight .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */ 54 | div.highlight .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */ 55 | div.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ 56 | div.highlight .sc { color: #4e9a06 } /* Literal.String.Char */ 57 | div.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ 58 | div.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ 59 | div.highlight .se { color: #4e9a06 } /* Literal.String.Escape */ 60 | div.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ 61 | div.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ 62 | div.highlight .sx { color: #4e9a06 } /* Literal.String.Other */ 63 | div.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ 64 | div.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ 65 | div.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ 66 | div.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ 67 | div.highlight .vc { color: #000000 } /* Name.Variable.Class */ 68 | div.highlight .vg { color: #000000 } /* Name.Variable.Global */ 69 | div.highlight .vi { color: #000000 } /* Name.Variable.Instance */ 70 | div.highlight .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */ 71 | -------------------------------------------------------------------------------- /pypress/themes/default/static/post-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/themes/default/static/post-line.png -------------------------------------------------------------------------------- /pypress/themes/default/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Login") }}{% end %} 3 | {% block content %} 4 |
5 |
6 |

{{ _("Login") }}

7 | {% from pypress.filters import field_errors %} 8 |
9 | {{ xsrf_form_html() }} 10 |
11 | {{ form.login.label() }} 12 | {{ form.login(class_="text") }} 13 | {{ field_errors(form.login) }} 14 |
15 |
16 | {{ form.password.label() }} 17 | {{ form.password(class_="text") }} 18 |
19 |
20 | {{ form.submit(class_="form-button") }} 21 | {{ field_errors(form.submit) }} 22 |
23 |
24 |
25 |
26 |

{{ _("Not a member?") }}

27 |

28 | 29 |

30 |
31 |
32 |
33 | {% end %} 34 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Signup") }}{% end %} 3 | {% block content %} 4 |
5 |
6 |

{{ _("Signup") }}

7 | {% from pypress.filters import field_errors %} 8 |
9 | {{ xsrf_form_html() }} 10 | {{ form.hidden_tag() }} 11 |
12 | {{ form.username.label() }} 13 | {{ form.username(class_="text") }} 14 | {{ field_errors(form.username) }} 15 |
16 |
17 | {{ form.nickname.label() }} 18 | {{ form.nickname(class_="text") }} 19 | {{ field_errors(form.nickname) }} 20 |
21 |
22 | {{ form.password.label() }} 23 | {{ form.password(class_="text") }} 24 | {{ field_errors(form.password) }} 25 |
26 |
27 | {{ form.password_again.label() }} 28 | {{ form.password_again(class_="text") }} 29 | {{ field_errors(form.password_again) }} 30 |
31 |
32 | {{ form.email.label() }} 33 | {{ form.email(class_="text") }} 34 | {{ field_errors(form.email) }} 35 |
36 |
37 | {{ form.code.label() }} 38 | {{ form.code(class_="text") }} 39 | {{ field_errors(form.code) }} 40 |
41 |
{{ form.submit(class_="form-button") }}
42 |
43 |
44 |
45 |

{{ _("As a Poster, you can...") }}

46 |

....

47 |

48 | 49 |

50 |
51 |
52 |
53 | {% end %} 54 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% end %} - Pypress by tornado 6 | 7 | {% block css %}{% end %} 8 | 9 | 26 | {% block js %}{% end %} 27 | 28 | 29 |
30 | {% set messages = handler.get_flashed_messages() %} 31 | {% if messages %} 32 |
33 | {% for category, msg in messages %} 34 | {{ msg }} 35 | {% end %} 36 |
37 | {% end %} 38 | 67 |
68 |
69 |
70 | {% block content %}{% end %} 71 |
72 | 75 |
76 |
77 | 82 |
83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/blog/add_comment.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Add comment") }}{% end %} 3 | {% block content %} 4 |
5 |

{{ post.title }}

6 |
7 |
8 | {% from pypress.filters import field_errors %} 9 | {{ xsrf_form_html() }} 10 | 11 | 12 | {% if current_user %} 13 | {{ form.email(type='hidden',value=current_user.email) }} 14 | {{ form.nickname(type='hidden',value=current_user.nickname) }} 15 | {% else %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% end %} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
{{ form.email.label }}{{ form.email(size=50, class_="text") }} {{ render_errors(form.email) }}
{{ form.nickname.label }}{{ form.nickname(size=50, class_="text") }} {{ render_errors(form.nickname) }}
{{ form.website.label }}{{ form.website(size=50, class_="text") }} {{ render_errors(form.website) }}
{{ form.comment.label }}{{ form.comment(class_="text", style="width:400px;height:100px;") }} {{ render_errors(form.comment) }}
{{ form.submit(class_="button") }}
39 |
40 |
41 |
42 | {% end %} 43 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/blog/archives.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Archives") }}{% end %} 3 | {% block content %} 4 |

Archives

5 |
6 | {% for index, post in enumerate(page_obj.items) %} 7 | {% if index==0 %} 8 |

{{ post.created_date.year }}

9 |
    10 | {% end %} 11 |
  • 12 | {{ locale.format_day(post.created_date) }} 13 | {{ post.title }} 14 |
  • 15 | {% if post.prev_post and post.created_date.year != post.prev_post.created_date.year %} 16 |
17 |

{{ post.prev_post.created_date.year }}

18 |
    19 | {% end %} 20 | {% end %} 21 |
22 |
23 | {% end %} 24 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/blog/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Edit") }}{% end %} 3 | {% block js %} 4 | 5 | 6 | 18 | {% end %} 19 | {% block content %} 20 |
21 |

{{ _("Edit") }}

22 | {% from pypress.filters import field_errors %} 23 |
24 | {{ xsrf_form_html() }} 25 |
26 | {{ form.title.label() }} 27 | {{ form.title(class_="text") }} 28 | {{ field_errors(form.title) }} 29 |
30 |
31 | {{ form.slug.label() }} 32 | {{ form.slug(class_="text") }} 33 | {{ field_errors(form.slug) }} 34 |
35 |
36 | {{ form.content.label() }} 37 | {{ form.content() }} 38 | {{ field_errors(form.content) }} 39 |
40 |
41 | {{ form.tags.label() }} 42 | {{ form.tags(class_="text") }} 43 | {{ field_errors(form.tags) }} 44 |
45 |
{{ form.submit(class_="form-button") }}
46 |
47 |
48 | {% end %} 49 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/blog/list.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("List") }}{% end %} 3 | {% block css %} 4 | 5 | {% end %} 6 | {% block content %} 7 |
8 | {% if page_obj.items %} 9 | {% for post in page_obj.items %} 10 | {% module Post(post) %} 11 | {% end %} 12 | {% else %} 13 |
No post
14 | {% end %} 15 |
16 | {% end %} 17 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/blog/module-comment.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {% from pypress.helpers import gravatar %} 3 |
    4 | {{ comment.author.nickname }} 5 |
    6 |
    7 | {{ comment.author.nickname }} 8 | {{ locale.format_date(comment.created_date) }} 9 |
    10 |
    11 | {{ comment.markdown }} 12 | 13 | {% if current_user %} 14 |
    15 | {% if comment.permissions.reply.can(handler.identity) and comment.parent_id is None %} 16 | {{ _("reply") }} 17 | {% end %} 18 | {% if comment.permissions.delete.can(handler.identity) %} 19 | {{ _("delete") }} 20 | 25 | {% end %} 26 |
    27 | 28 | {% end %} 29 |
    30 | 31 | {% if comment.comments %} 32 |
    33 |
      34 | {% for child_comment in comment.comments %} 35 | {% module Comment(child_comment, form) %} 36 | {% end %} 37 |
    38 |
    39 | {% end %} 40 |
  • 41 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/blog/module-post.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ post.title }}

    3 | 9 |
    10 | {% from pypress.helpers import code_highlight %} 11 | {{ code_highlight(post.summary) }} 12 | {% if post.summary!=post.content %}Read more...{% end %} 13 |
    14 |
    15 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/blog/people.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ people.username }}{% end %} 3 | {% block css %} 4 | 5 | {% end %} 6 | {% block content %} 7 |

    People: {{ people.username }}

    8 |
    9 | {% if page_obj.items %} 10 | {% for post in page_obj.items %} 11 | {% module Post(post) %} 12 | {% end %} 13 | {% else %} 14 |
    No post
    15 | {% end %} 16 |
    17 | {% end %} 18 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/blog/post.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Post Now") }}{% end %} 3 | {% block js %} 4 | 5 | 6 | 18 | {% end %} 19 | {% block content %} 20 |
    21 |

    {{ _("Post Now") }}

    22 | {% from pypress.filters import field_errors %} 23 |
    24 | {{ xsrf_form_html() }} 25 |
    26 | {{ form.title.label() }} 27 | {{ form.title(class_="text") }} 28 | {{ field_errors(form.title) }} 29 |
    30 |
    31 | {{ form.slug.label() }} 32 | {{ form.slug(class_="text") }} 33 | {{ field_errors(form.slug) }} 34 |
    35 |
    36 | {{ form.content.label() }} 37 | {{ form.content() }} 38 | {{ field_errors(form.content) }} 39 |
    40 |
    41 | {{ form.tags.label() }} 42 | {{ form.tags(class_="text") }} 43 | {{ field_errors(form.tags) }} 44 |
    45 |
    {{ form.submit(class_="form-button") }}
    46 |
    47 |
    48 | {% end %} 49 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/blog/search.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _('Search') }}{% end %} 3 | {% block css %} 4 | 5 | {% end %} 6 | {% block content %} 7 |

    search by {{ keywords }}

    8 |
    9 | {% if page_obj.items %} 10 | {% for post in page_obj.items %} 11 | {% module Post(post) %} 12 | {% end %} 13 | {% else %} 14 |
    No post
    15 | {% end %} 16 |
    17 | {% end %} 18 | 19 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/blog/tags.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}Tags{% end %} 3 | {% block content %} 4 |

    Tags

    5 |
    6 | 11 |
    12 | {% end %} 13 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/blog/view.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ post.title }}{% end %} 3 | {% block css %} 4 | 5 | {% end %} 6 | {% block js %} 7 | 8 | 13 | {% end %} 14 | {% block content %} 15 |
    16 |

    {{ post.title }}

    17 | 28 |
    29 | {% from pypress.helpers import code_highlight %} 30 | {{ code_highlight(post.content) }} 31 |
    32 |
    33 | {% if post.prev_post %}prev: {{ post.prev_post.title }}{% end %} 34 | {% if post.next_post %}next: {{ post.next_post.title }}{% end %} 35 |
    36 |
    37 |
    38 |
    39 |

    {{ _('Add a comment') }}

    40 |
    41 | {% from pypress.filters import field_errors %} 42 | {{ xsrf_form_html() }} 43 | {% if current_user %} 44 | {{ form.email(type='hidden',value=current_user.email) }} 45 | {{ form.nickname(type='hidden',value=current_user.nickname) }} 46 | {{ form.website(type='hidden') }} 47 | {% else %} 48 |

    49 | {{ form.email.label() }} 50 | {{ form.email(class_="text") }} 51 | {{ field_errors(form.email) }} 52 |

    53 |

    54 | {{ form.nickname.label() }} 55 | {{ form.nickname(class_="text") }} 56 | {{ field_errors(form.nickname) }} 57 |

    58 |

    59 | {{ form.website.label() }} 60 | {{ form.website(class_="text") }} 61 | {{ field_errors(form.website) }} 62 |

    63 | {% end %} 64 |

    65 | {{ form.comment.label() }} 66 | {{ form.comment(class_="text") }} 67 | {{ field_errors(form.comment) }} 68 |

    69 |

    70 | {{ form.captcha.label() }} 71 | {{ form.captcha(class_="text") }} 72 | 73 | {{ field_errors(form.captcha) }} 74 |

    75 |

    {{ form.submit(class_="button") }}

    76 |
    77 |
    78 | 79 |
    80 |

    {{ _('Comments') }}

    81 | {% if post.comments %} 82 |
      83 | {% for comment in post.comments %} 84 | {% module Comment(comment, form) %} 85 | {% end %} 86 |
    87 | {% else %} 88 |

    {{ _('No comments have been posted yet.') }}

    89 | {% end %} 90 |
    91 | 92 |
    93 | {% end %} 94 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 6 | 7 | 8 | 404: Page not found 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 500 6 | 7 | 8 | 500: Server error! 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/errors/exception.htm: -------------------------------------------------------------------------------- 1 | {% import sys %} 2 | {% import os %} 3 | {% import tornado %} 4 | {% import traceback %} 5 | {% set type, value, tback = sys.exc_info() %} 6 | 7 | 8 | 9 | 10 | HTTP Status {{ status_code }} » Tornado v{{ tornado.version }} 11 | 80 | 81 | 82 | 83 | 86 | 87 | {% if exception %} 88 | {% set traceback_list = traceback.extract_tb(tback) %} 89 | {% set filepath, line, method, code = traceback_list[-1] %} 90 | 91 |
    92 |

    Application raised {{ exception.__class__.__name__ }}: {{ exception }}

    93 | 94 | 95 | 98 | 99 | 100 | 101 | 109 | 110 |
    96 |

    on line {{ line }} of {{ method }} in {{ os.path.basename(filepath) }}

    97 |
    File: {{ filepath }}
    102 | {% set extension = os.path.splitext(filepath)[1][1:] %} 103 | {% if extension in ['py', 'html', 'htm'] %} 104 | {{ get_snippet(filepath, line, 10) }} 105 | {% else %} 106 |

    Cannot load file, type not supported.

    107 | {% end %} 108 |
    111 | 112 |

    Full Traceback

    113 |
    114 | {% for filepath, line, method, code in traceback_list %} 115 | 116 | 117 | 120 | 121 | 122 | 123 | 131 | 132 |
    118 |

    on line {{ line }} of {{ method }} in {{ os.path.basename(filepath) }}

    119 |
    File: {{ filepath }}
    124 | {% set extension = os.path.splitext(filepath)[1][1:] %} 125 | {% if extension in ['py', 'html', 'htm'] %} 126 | {{ get_snippet(filepath, line, 10) }} 127 | {% else %} 128 |

    Cannot load file, type not supported.

    129 | {% end %} 130 |
    133 |
    134 | {% end %} 135 |
    136 | 137 |

    Request Headers

    138 |
    139 | 140 | {% for hk, hv in handler.request.headers.iteritems() %} 141 | 142 | 143 | 144 | 145 | {% end %} 146 |
    {{ hk }}{{ hv }}
    147 |
    148 | 149 |

    Response Headers

    150 |
    151 | 152 | {% for hk, hv in handler._headers.iteritems() %} 153 | 154 | 155 | 156 | 157 | {% end %} 158 |
    {{ hk }}{{ hv }}
    159 |
    160 |
    161 | {% end %} 162 | 163 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/macros/_field_errors.html: -------------------------------------------------------------------------------- 1 | {% if field.errors %} 2 |
      3 | {% for error in field.errors %} 4 |
    • {{ error }}
    • 5 | {% end %} 6 |
    7 | {% end %} 8 | 9 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/macros/_page.html: -------------------------------------------------------------------------------- 1 | {% if page_obj.pages > 1 %} 2 | 17 | {% endif %} 18 | -------------------------------------------------------------------------------- /pypress/themes/simple/static/base.css: -------------------------------------------------------------------------------- 1 | html,body,h1,h2,h3,h4,h5,h6,td,th,dd,p,div,ol,ul,li { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | font: 0.75em/1.5em 'Georgia','Verdana','Arial',sans-serif; 8 | color: #333; 9 | background: #ececec url('bg.gif') repeat 0 0; 10 | } 11 | 12 | /***** public class settings ****/ 13 | 14 | h1 { 15 | font-size: 20px; 16 | line-height: 2em; 17 | } 18 | h2 { 19 | font-size: 14px; 20 | line-height: 2em; 21 | } 22 | h3 { 23 | font-size: 12px; 24 | line-height: 1.6em; 25 | } 26 | h4,h5,h6 { 27 | font-size: 12px; 28 | line-height: 1.5em; 29 | } 30 | 31 | input, textarea { 32 | font-size: 14px; 33 | } 34 | 35 | a:link, a:visited { 36 | color: #06c; 37 | text-decoration: none; 38 | } 39 | a:hover { 40 | color: #2f80d2; 41 | } 42 | 43 | a img { 44 | border: 0; 45 | } 46 | 47 | blockquote { 48 | color: #555; 49 | font-style: italic; 50 | font-size: 17px; 51 | } 52 | 53 | .clearfix:before,.clearfix:after { 54 | content:"."; 55 | display:block; 56 | height:0; 57 | overflow:hidden; 58 | } 59 | 60 | .clearfix:after, .clear { 61 | clear:both; 62 | } 63 | 64 | .clearfix{ 65 | zoom:1; /* IE < 8 */ 66 | } 67 | 68 | .error-no-data { 69 | line-height: 1.6em; 70 | } 71 | .errors { 72 | clear: both; 73 | color: #e00; 74 | } 75 | 76 | #flashed { 77 | padding: 5px; 78 | color: #f60; 79 | background: #eeefce; 80 | text-align: center; 81 | } 82 | 83 | #go-to-top { 84 | display: block; 85 | position: fixed; 86 | right: 60px; 87 | bottom: 100px; 88 | padding: 18px; 89 | color: #666; 90 | background: #ddd; 91 | font: 36px/18px Helvetica,Arial,Verdana,sans-serif; 92 | opacity: 0.7; 93 | outline: 0 none; 94 | text-decoration: none; 95 | text-shadow: 0 0 1px #ddd; 96 | vertical-align: baseline; 97 | -moz-border-radius: 5px; 98 | -khtml-border-radius: 5px; 99 | -webkit-border-radius: 5px; 100 | border-radius: 5px; 101 | } 102 | 103 | 104 | #wrap { 105 | margin: auto; 106 | } 107 | 108 | #header { 109 | height: 100%; 110 | left: 0; 111 | position: fixed; 112 | top: 0; 113 | width: 200px; 114 | } 115 | #header .inner { 116 | padding: 20px 0 20px 40px; 117 | } 118 | #header .desc { 119 | margin-bottom: 20px; 120 | color: #666; 121 | } 122 | 123 | #menu { 124 | position: absolute; 125 | right: 40px; 126 | top: 20px; 127 | } 128 | 129 | 130 | #nav li { 131 | list-style: none; 132 | line-height: 1.8em; 133 | } 134 | 135 | #container { 136 | padding: 2em; 137 | margin-left: 200px; 138 | min-width: 820px; 139 | } 140 | .content { 141 | float: left; 142 | padding: 10px 0; 143 | width: 600px; 144 | } 145 | .content h2 { 146 | margin-bottom: 10px; 147 | font-size: 16px; 148 | color: black; 149 | } 150 | .sidebar { 151 | float: left; 152 | padding: 40px 0 0 40px; 153 | width: 180px; 154 | } 155 | .sidebar-module { 156 | margin-bottom: 20px; 157 | } 158 | .sidebar-module h3 { 159 | padding: 10px 0; 160 | } 161 | .sidebar-module ul li { 162 | list-style: none; 163 | padding: 5px 0; 164 | border-bottom: 1px dotted #ccc; 165 | } 166 | .sidebar-module-foot { 167 | padding: 10px 0; 168 | } 169 | 170 | .sidebar #search input.text { 171 | padding: 2px; 172 | width: 160px; 173 | border: 1px solid #ccc; 174 | background: #fff url('find.png') no-repeat right 50%; 175 | } 176 | .sidebar #tags li { 177 | display: inline; 178 | border: 0; 179 | padding-right: 10px; 180 | } 181 | 182 | #post-form input.text { 183 | width: 500px; 184 | } 185 | 186 | #post-form textarea.text { 187 | width: 600px; 188 | height: 300px; 189 | } 190 | 191 | /***** form *****/ 192 | .form h2 { 193 | margin-bottom: 12px; 194 | } 195 | .form-field { 196 | clear: both; 197 | padding: 5px 0; 198 | } 199 | .form-field label { 200 | display: block; 201 | } 202 | .form-field .text { 203 | padding: 2px; 204 | width: 200px; 205 | } 206 | .form-button { 207 | cursor: pointer; 208 | background: #EA4C89 url("glass-light.png") repeat-x scroll 0 50%; 209 | border: medium none; 210 | -moz-border-radius: 6px; 211 | -khtml-border-radius: 6px; 212 | -webkit-border-radius: 6px; 213 | border-radius: 6px; 214 | color: #FFFFFF; 215 | font-weight: bold; 216 | padding: 6px 24px; 217 | text-decoration: none; 218 | } 219 | .form-button:hover, .form-button:focus { 220 | background-color: #DF3E7B; 221 | text-decoration: none; 222 | } 223 | 224 | .form-section { 225 | margin:210px 0; 226 | } 227 | 228 | .form-section label { 229 | display: block; 230 | font-size: 14px; 231 | line-height: 1.6em; 232 | } 233 | 234 | .form-section input.text { 235 | width: 70%; 236 | } 237 | 238 | .form-section textarea { 239 | width: 100%; 240 | height: 300px; 241 | } 242 | 243 | .form-section input.submit { 244 | font-size: 16px; 245 | line-height: 1.6em; 246 | } 247 | 248 | #login-box .text { 249 | height: 16px; 250 | line-height: 16px; 251 | } 252 | 253 | #login-box .form-button, 254 | #signup-box .form-button { 255 | background-color: #4D90FE; 256 | } 257 | 258 | #post-view h2 { 259 | margin-bottom: 0; 260 | } 261 | #post-list { 262 | padding: 5px 0; 263 | } 264 | 265 | .post { 266 | margin-bottom: 20px; 267 | border-bottom: 1px solid #ccc; 268 | } 269 | .post h2 { 270 | margin-bottom: 0; 271 | font-size: 13px; 272 | } 273 | 274 | .post-avatar a { 275 | display: block; 276 | } 277 | 278 | .post-content { 279 | margin-bottom: 10px; 280 | padding: 10px 0; 281 | font-size: 12px; 282 | line-height: 1.6em; 283 | } 284 | .post-content hr { 285 | display: none; 286 | } 287 | .post-content p { 288 | margin: 10px auto; 289 | } 290 | .post-content ol, .post-content ul { 291 | margin: 20px 30px; 292 | } 293 | .post-content pre { 294 | display: block; 295 | padding: 15px; 296 | border-color: #B3B3B3 #B3B3B3 #B3B3B3 #DDDDDD; 297 | border-style: dotted dotted dotted solid; 298 | border-width: 1px 1px 1px 5px; 299 | background: #fff; 300 | font-size: 0.9em; 301 | } 302 | 303 | .post-meta { 304 | padding: 5px 0; 305 | color: #999; 306 | } 307 | .post-meta span { 308 | margin-right: 10px; 309 | text-transform: capitalize; 310 | } 311 | .post-meta a:link, 312 | .post-meta a:visited { 313 | color: #666; 314 | } 315 | .post-meta a:hover { 316 | color: #555; 317 | } 318 | 319 | .post-foot { 320 | margin-bottom: 10px; 321 | padding: 5px 10px; 322 | height: 20px; 323 | line-height: 20px; 324 | background: #ddd; 325 | } 326 | .post-foot .prev { 327 | float: left; 328 | } 329 | .post-foot .next { 330 | float: right; 331 | } 332 | .comment-add h3, 333 | .comment-list h3 { 334 | padding: 10px 0; 335 | } 336 | .comment-add div { 337 | margin-bottom: 10px; 338 | } 339 | .comment-add div label { 340 | display: block; 341 | line-height: 24px; 342 | } 343 | .comment-add div a img { 344 | vertical-align: -6px; 345 | } 346 | .comment-add textarea { 347 | margin: 5px 0 0; 348 | padding: 3px 4px; 349 | width: 492px; 350 | height: 106px; 351 | font-size: 13px; 352 | line-height: 17px; 353 | } 354 | .comment-add input.text { 355 | width: 300px; 356 | } 357 | .comment-add p.explain { 358 | color: #999; 359 | } 360 | 361 | .comment-list ol { 362 | list-style: none; 363 | padding: 0; 364 | } 365 | .comment-list ol ol { 366 | margin-left: 70px; 367 | } 368 | .comment { 369 | margin: 15px 0; 370 | padding-top: 15px; 371 | width: 100%; 372 | border-top: 1px solid #ddd; 373 | } 374 | .comment-avatar { 375 | float: left; 376 | height: 60px; 377 | margin: 0 10px 0 0; 378 | overflow: hidden; 379 | position: relative; 380 | width: 60px; 381 | border: 1px solid #ddd; 382 | } 383 | .comment-avatar a { 384 | float: left; 385 | display: block; 386 | padding: 5px; 387 | } 388 | .comment-text { 389 | margin: 0 0 0 72px; 390 | } 391 | 392 | #tag-cloud { 393 | padding: 20px 0; 394 | } 395 | #tag-cloud ul { 396 | padding: 0; 397 | } 398 | #tag-cloud li { 399 | display: inline; 400 | padding: 0 5px; 401 | } 402 | a.tag-0 { color:#aaa; font-size: 0.9em; font-weight: 100; } 403 | a.tag-1 { color:#999; font-size: 0.9em; font-weight: 100; } 404 | a.tag-2 { color:#999; font-size: 1em; font-weight: 200; } 405 | a.tag-3 { color:#888; font-size: 1.1em; font-weight: 300; } 406 | a.tag-4 { color:#777; font-size: 1.2em; font-weight: 400; } 407 | a.tag-5 { color:#666; font-size: 1.3em; font-weight: 500; } 408 | a.tag-6 { color:#555; font-size: 1.4em; font-weight: 600; } 409 | a.tag-7 { color:#333; font-size: 1.6em; font-weight: 700; } 410 | a.tag-8 { color:#000; font-size: 1.8em; font-weight: 800; } 411 | a.tag-9 { color:#06c; font-size: 2em; font-weight: 900; } 412 | a.tag-10 { color:#c60; font-size: 2em; font-weight: 900; } 413 | 414 | .archives h3 { 415 | margin: 10px 0; 416 | border-bottom: 1px solid #ccc; 417 | font-size: 1.5em; 418 | line-height: 2em; 419 | font-weight: normal; 420 | } 421 | .archives h3 a:link, 422 | .archives h3 a:visited { 423 | color: #555; 424 | } 425 | .archives h3 a:hover { 426 | color: #999; 427 | } 428 | 429 | .archives ul, .links ul { 430 | margin-left: 20px; 431 | } 432 | .archives ul li, .links ul li { 433 | list-style-type: square; 434 | line-height: 2em; 435 | } 436 | .archives ul li span { 437 | float: right; 438 | color: #999; 439 | } 440 | 441 | .links ul li { 442 | position: relative; 443 | padding-right: 20px; 444 | } 445 | .links ul li:hover { 446 | background: #ddd; 447 | } 448 | .links ul li span { 449 | position: absolute; 450 | right: 0; 451 | } 452 | .pagination span { 453 | font-size: 14px; 454 | } 455 | .pagination span.previous { 456 | float: left; 457 | } 458 | .pagination span.next { 459 | float: right; 460 | } 461 | 462 | /***** footer *****/ 463 | #footer { 464 | padding: 10px 40px 20px; 465 | color: #999; 466 | text-align: right; 467 | font-size: 11px; 468 | } 469 | 470 | -------------------------------------------------------------------------------- /pypress/themes/simple/static/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/themes/simple/static/bg.gif -------------------------------------------------------------------------------- /pypress/themes/simple/static/blog.js: -------------------------------------------------------------------------------- 1 | var get_cookie = function(name) { 2 | var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); 3 | return r ? r[1] : undefined; 4 | } 5 | 6 | var message = function(message, category){ 7 | $('#flashed').append('' + message + ''); 8 | } 9 | 10 | var ajax_post = function(url, params, on_success){ 11 | var _callback = function(response){ 12 | if (response.success) { 13 | if (response.redirect_url){ 14 | window.location.href = response.redirect_url; 15 | } else if (response.reload){ 16 | window.location.reload(); 17 | } else if (on_success) { 18 | return on_success(response); 19 | } 20 | } else { 21 | return message(response.error, "error"); 22 | } 23 | } 24 | $.post(url, params, _callback, "json"); 25 | } 26 | 27 | var delete_comment = function(url) { 28 | var callback = function(response){ 29 | $('#comment-' + response.comment_id).remove(); 30 | } 31 | var params = {'_xsrf': get_cookie("_xsrf")}; 32 | ajax_post(url, params, callback); 33 | } 34 | 35 | var get_captcha = function(url) { 36 | var uri = url + '?t=' + String(Math.random()).slice(3,8); 37 | $("#captcha-image").attr('src', uri); 38 | } 39 | -------------------------------------------------------------------------------- /pypress/themes/simple/static/find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/themes/simple/static/find.png -------------------------------------------------------------------------------- /pypress/themes/simple/static/glass-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/themes/simple/static/glass-light.png -------------------------------------------------------------------------------- /pypress/themes/simple/static/highlight.css: -------------------------------------------------------------------------------- 1 | div.highlight { margin: 0px; overflow: auto; width: 100%; } 2 | div.highlight .hll { background-color: #ffffcc } 3 | div.highlight .c { color: #8f5902; font-style: italic } /* Comment */ 4 | div.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ 5 | div.highlight .g { color: #000000 } /* Generic */ 6 | div.highlight .k { color: #204a87; font-weight: bold } /* Keyword */ 7 | div.highlight .l { color: #000000 } /* Literal */ 8 | div.highlight .n { color: #000000 } /* Name */ 9 | div.highlight .o { color: #ce5c00; font-weight: bold } /* Operator */ 10 | div.highlight .x { color: #000000 } /* Other */ 11 | div.highlight .p { color: #000000; font-weight: bold } /* Punctuation */ 12 | div.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ 13 | div.highlight .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */ 14 | div.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ 15 | div.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ 16 | div.highlight .gd { color: #a40000 } /* Generic.Deleted */ 17 | div.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ 18 | div.highlight .gr { color: #ef2929 } /* Generic.Error */ 19 | div.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 20 | div.highlight .gi { color: #00A000 } /* Generic.Inserted */ 21 | div.highlight .go { color: #000000; font-style: italic } /* Generic.Output */ 22 | div.highlight .gp { color: #8f5902 } /* Generic.Prompt */ 23 | div.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ 24 | div.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 25 | div.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ 26 | div.highlight .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */ 27 | div.highlight .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */ 28 | div.highlight .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */ 29 | div.highlight .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */ 30 | div.highlight .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */ 31 | div.highlight .kt { color: #204a87; font-weight: bold } /* Keyword.Type */ 32 | div.highlight .ld { color: #000000 } /* Literal.Date */ 33 | div.highlight .m { color: #0000cf; font-weight: bold } /* Literal.Number */ 34 | div.highlight .s { color: #4e9a06 } /* Literal.String */ 35 | div.highlight .na { color: #c4a000 } /* Name.Attribute */ 36 | div.highlight .nb { color: #204a87 } /* Name.Builtin */ 37 | div.highlight .nc { color: #000000 } /* Name.Class */ 38 | div.highlight .no { color: #000000 } /* Name.Constant */ 39 | div.highlight .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */ 40 | div.highlight .ni { color: #ce5c00 } /* Name.Entity */ 41 | div.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ 42 | div.highlight .nf { color: #000000 } /* Name.Function */ 43 | div.highlight .nl { color: #f57900 } /* Name.Label */ 44 | div.highlight .nn { color: #000000 } /* Name.Namespace */ 45 | div.highlight .nx { color: #000000 } /* Name.Other */ 46 | div.highlight .py { color: #000000 } /* Name.Property */ 47 | div.highlight .nt { color: #204a87; font-weight: bold } /* Name.Tag */ 48 | div.highlight .nv { color: #000000 } /* Name.Variable */ 49 | div.highlight .ow { color: #204a87; font-weight: bold } /* Operator.Word */ 50 | div.highlight .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ 51 | div.highlight .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */ 52 | div.highlight .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */ 53 | div.highlight .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */ 54 | div.highlight .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */ 55 | div.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ 56 | div.highlight .sc { color: #4e9a06 } /* Literal.String.Char */ 57 | div.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ 58 | div.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ 59 | div.highlight .se { color: #4e9a06 } /* Literal.String.Escape */ 60 | div.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ 61 | div.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ 62 | div.highlight .sx { color: #4e9a06 } /* Literal.String.Other */ 63 | div.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ 64 | div.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ 65 | div.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ 66 | div.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ 67 | div.highlight .vc { color: #000000 } /* Name.Variable.Class */ 68 | div.highlight .vg { color: #000000 } /* Name.Variable.Global */ 69 | div.highlight .vi { color: #000000 } /* Name.Variable.Instance */ 70 | div.highlight .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */ 71 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Login") }}{% end %} 3 | {% block content %} 4 |
    5 |
    6 |

    {{ _("Login") }}

    7 | {% from pypress.filters import field_errors %} 8 |
    9 | {{ xsrf_form_html() }} 10 |
    11 | {{ form.login.label() }} 12 | {{ form.login(class_="text") }} 13 | {{ field_errors(form.login) }} 14 |
    15 |
    16 | {{ form.password.label() }} 17 | {{ form.password(class_="text") }} 18 |
    19 |
    20 | {{ form.submit(class_="form-button") }} 21 | {{ field_errors(form.submit) }} 22 |
    23 |
    24 |
    25 |
    26 |

    {{ _("Not a member?") }}

    27 |

    28 | 29 |

    30 |
    31 |
    32 |
    33 | {% end %} 34 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Signup") }}{% end %} 3 | {% block content %} 4 |
    5 |
    6 |

    {{ _("Signup") }}

    7 | {% from pypress.filters import field_errors %} 8 |
    9 | {{ xsrf_form_html() }} 10 | {{ form.hidden_tag() }} 11 |
    12 | {{ form.username.label() }} 13 | {{ form.username(class_="text") }} 14 | {{ field_errors(form.username) }} 15 |
    16 |
    17 | {{ form.nickname.label() }} 18 | {{ form.nickname(class_="text") }} 19 | {{ field_errors(form.nickname) }} 20 |
    21 |
    22 | {{ form.password.label() }} 23 | {{ form.password(class_="text") }} 24 | {{ field_errors(form.password) }} 25 |
    26 |
    27 | {{ form.password_again.label() }} 28 | {{ form.password_again(class_="text") }} 29 | {{ field_errors(form.password_again) }} 30 |
    31 |
    32 | {{ form.email.label() }} 33 | {{ form.email(class_="text") }} 34 | {{ field_errors(form.email) }} 35 |
    36 |
    37 | {{ form.code.label() }} 38 | {{ form.code(class_="text") }} 39 | {{ field_errors(form.code) }} 40 |
    41 |
    {{ form.submit(class_="form-button") }}
    42 |
    43 |
    44 |
    45 |

    {{ _("As a Poster, you can...") }}

    46 |

    ....

    47 |

    48 | 49 |

    50 |
    51 |
    52 |
    53 | {% end %} 54 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% end %} - Pypress by tornado 6 | 7 | {% block css %}{% end %} 8 | 9 | 26 | {% block js %}{% end %} 27 | 41 | 42 | 43 |
    44 | {% set messages = handler.get_flashed_messages() %} 45 | {% if messages %} 46 |
    47 | {% for category, msg in messages %} 48 | {{ msg }} 49 | {% end %} 50 |
    51 | {% end %} 52 | 77 | 85 |
    86 |
    87 |
    88 | {% block content %}{% end %} 89 |
    90 | {% block sidebar %} 91 | 150 | {% end %} 151 |
    152 |
    153 | 158 |
    159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/add_comment.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Add comment") }}{% end %} 3 | {% block content %} 4 |
    5 |

    {{ post.title }}

    6 |
    7 |
    8 | {% from pypress.filters import field_errors %} 9 | {{ xsrf_form_html() }} 10 | 11 | 12 | {% if current_user %} 13 | {{ form.email(type='hidden',value=current_user.email) }} 14 | {{ form.nickname(type='hidden',value=current_user.nickname) }} 15 | {% else %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% end %} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
    {{ form.email.label }}{{ form.email(size=50, class_="text") }} {{ render_errors(form.email) }}
    {{ form.nickname.label }}{{ form.nickname(size=50, class_="text") }} {{ render_errors(form.nickname) }}
    {{ form.website.label }}{{ form.website(size=50, class_="text") }} {{ render_errors(form.website) }}
    {{ form.comment.label }}{{ form.comment(class_="text", style="width:400px;height:100px;") }} {{ render_errors(form.comment) }}
    {{ form.submit(class_="button") }}
    39 |
    40 |
    41 |
    42 | {% end %} 43 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/archives.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Archives") }}{% end %} 3 | {% block content %} 4 |

    {{ _("Archives") }}

    5 |
    6 | {% for index, post in enumerate(page_obj.items) %} 7 | {% if index==0 %} 8 |

    {{ post.created_date.year }}

    9 |
      10 | {% end %} 11 |
    • 12 | {{ post.author.nickname }}, {{ locale.format_day(post.created_date, dow=False) }} 13 | {{ post.title }} 14 |
    • 15 | {% if post.prev_post and post.created_date.year != post.prev_post.created_date.year %} 16 |
    17 |

    {{ post.prev_post.created_date.year }}

    18 |
      19 | {% end %} 20 | {% end %} 21 |
    22 | {% module Paginate(page_obj, page_url) %} 23 |
    24 | {% end %} 25 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Edit") }}{% end %} 3 | {% block js %} 4 | 5 | 6 | 7 | 8 | 11 | 23 | {% end %} 24 | {% block content %} 25 |
    26 |

    {{ _("Edit") }}

    27 | {% from pypress.filters import field_errors %} 28 |
    29 | {{ xsrf_form_html() }} 30 |
    31 | {{ form.title.label() }} 32 | {{ form.title(class_="text") }} 33 | {{ field_errors(form.title) }} 34 |
    35 |
    36 | {{ form.slug.label() }} 37 | {{ form.slug(class_="text") }} 38 | {{ field_errors(form.slug) }} 39 |
    40 |
    41 | {{ form.content.label() }} 42 | {{ form.content() }} 43 | {{ field_errors(form.content) }} 44 |
    45 |
    46 | {{ form.tags.label() }} 47 | {{ form.tags(class_="text") }} 48 | {{ field_errors(form.tags) }} 49 |
    50 |
    {{ form.submit(class_="form-button") }}
    51 |
    52 |
    53 | {% end %} 54 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/list.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Archive") }}{% end %} 3 | {% block css %} 4 | 5 | {% end %} 6 | {% block content %} 7 | {% module List(page_obj, page_url) %} 8 | {% end %} 9 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/module-comment.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {% from pypress.helpers import gravatar %} 3 |
    4 | {{ comment.author.nickname }} 5 |
    6 |
    7 | {{ comment.author.nickname }} 8 | {{ locale.format_date(comment.created_date) }} 9 |
    10 |
    11 | {{ comment.markdown }} 12 | 13 | {% if current_user %} 14 | 25 | 26 | {% end %} 27 |
    28 | 29 | {% if comment.comments %} 30 |
    31 |
      32 | {% for child_comment in comment.comments %} 33 | {% module Comment(child_comment, form) %} 34 | {% end %} 35 |
    36 |
    37 | {% end %} 38 |
  • 39 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/module-list.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if page_obj.total %} 3 | {% for post in page_obj.items %} 4 | {% module Post(post) %} 5 | {% end %} 6 | {% module Paginate(page_obj, page_url) %} 7 | {% else %} 8 |
    No post yet. Post Now
    9 | {% end %} 10 |
    11 | 12 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/module-post.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ post.title }}

    3 | 9 |
    10 | {% from pypress.helpers import code_highlight %} 11 | {{ code_highlight(post.summary) }} 12 | {% if post.summary!=post.content %}Read more...{% end %} 13 |
    14 |
    15 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/people.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}People: {{ people.nickname }}{% end %} 3 | {% block css %} 4 | 5 | {% end %} 6 | {% block content %} 7 |

    People: {{ people.nickname }}

    8 | {% module List(page_obj, page_url) %} 9 | {% end %} 10 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/post.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Post Now") }}{% end %} 3 | {% block js %} 4 | 5 | 6 | 7 | 8 | 11 | 20 | {% end %} 21 | {% block content %} 22 |
    23 |

    {{ _("Post Now") }}

    24 | {% from pypress.filters import field_errors %} 25 |
    26 | {{ xsrf_form_html() }} 27 |
    28 | {{ form.title.label() }} 29 | {{ form.title(class_="text") }} 30 | {{ field_errors(form.title) }} 31 |
    32 |
    33 | {{ form.slug.label() }} 34 | {{ form.slug(class_="text") }} 35 | {{ field_errors(form.slug) }} 36 |
    37 |
    38 | {{ form.content.label() }} 39 | {{ form.content() }} 40 | {{ field_errors(form.content) }} 41 |
    42 |
    43 | {{ form.tags.label() }} 44 | {{ form.tags(class_="text") }} 45 | {{ field_errors(form.tags) }} 46 |
    47 |
    {{ form.submit(class_="form-button") }}
    48 |
    49 |
    50 | {% end %} 51 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/search.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _('Search') }}{% end %} 3 | {% block css %} 4 | 5 | {% end %} 6 | {% block content %} 7 |

    search by {{ keywords }}

    8 | {% module List(page_obj, page_url) %} 9 | {% end %} 10 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/tag.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}Tag: {{ tagname }}{% end %} 3 | {% block css %} 4 | 5 | {% end %} 6 | {% block content %} 7 |

    Tag: {{ tagname }}

    8 | {% module List(page_obj, page_url) %} 9 | {% end %} 10 | 11 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/tags.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}Tags{% end %} 3 | {% block content %} 4 |

    Tags

    5 |
    6 | 11 |
    12 | {% end %} 13 | {% block tag_cloud %}{% end %} 14 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/blog/view.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ post.title }}{% end %} 3 | {% block css %} 4 | 5 | {% end %} 6 | {% block js %} 7 | 8 | 24 | {% end %} 25 | {% block content %} 26 |
    27 |

    {{ post.title }}

    28 | 39 |
    40 | {% from pypress.helpers import code_highlight %} 41 | {{ code_highlight(post.content) }} 42 |
    43 |
    44 | {% if post.prev_post %}prev: {{ post.prev_post.title }}{% end %} 45 | {% if post.next_post %}next: {{ post.next_post.title }}{% end %} 46 |
    47 |
    48 |
    49 |
    50 |

    {{ _('Add a comment') }}

    51 |
    52 | {% from pypress.filters import field_errors %} 53 | {{ xsrf_form_html() }} 54 | {% if current_user %} 55 | {{ form.email(type='hidden',value=current_user.email) }} 56 | {{ form.nickname(type='hidden',value=current_user.nickname) }} 57 | {{ form.website(type='hidden') }} 58 | {% else %} 59 |
    60 | {{ form.email.label() }} 61 | {{ form.email(class_="text") }} 62 | {{ field_errors(form.email) }} 63 |
    64 |
    65 | {{ form.nickname.label() }} 66 | {{ form.nickname(class_="text") }} 67 | {{ field_errors(form.nickname) }} 68 |
    69 |
    70 | {{ form.website.label() }} 71 | {{ form.website(class_="text") }} 72 | {{ field_errors(form.website) }} 73 |
    74 | {% end %} 75 |
    76 | {{ form.comment.label() }} 77 | {{ form.comment(class_="text") }} 78 | {{ field_errors(form.comment) }} 79 |

    _italic_, __bold__, <a>: [example](http://url.com/ "Title"), <img>: ![alt text](/path/img.jpg "Title")

    80 |
    81 |
    82 | {{ form.captcha.label() }} 83 | {{ form.captcha(class_="text") }} 84 | 85 | {{ field_errors(form.captcha) }} 86 |
    87 |
    {{ form.submit(class_="button") }}
    88 |
    89 |
    90 | 91 |
    92 |

    {{ _('Comments') }}

    93 | {% if post.comments %} 94 |
      95 | {% for comment in post.comments %} 96 | {% module Comment(comment, form) %} 97 | {% end %} 98 |
    99 | {% else %} 100 |

    {{ _('No comments have been posted yet.') }}

    101 | {% end %} 102 |
    103 | 104 |
    105 | {% end %} 106 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 6 | 7 | 8 | 404: Page not found 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 500 6 | 7 | 8 | 500: Server error! 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/errors/exception.htm: -------------------------------------------------------------------------------- 1 | {% import sys %} 2 | {% import os %} 3 | {% import tornado %} 4 | {% import traceback %} 5 | {% set type, value, tback = sys.exc_info() %} 6 | 7 | 8 | 9 | 10 | HTTP Status {{ status_code }} » Tornado v{{ tornado.version }} 11 | 80 | 81 | 82 | 83 | 86 | 87 | {% if exception %} 88 | {% set traceback_list = traceback.extract_tb(tback) %} 89 | {% set filepath, line, method, code = traceback_list[-1] %} 90 | 91 |
    92 |

    Application raised {{ exception.__class__.__name__ }}: {{ exception }}

    93 | 94 | 95 | 98 | 99 | 100 | 101 | 109 | 110 |
    96 |

    on line {{ line }} of {{ method }} in {{ os.path.basename(filepath) }}

    97 |
    File: {{ filepath }}
    102 | {% set extension = os.path.splitext(filepath)[1][1:] %} 103 | {% if extension in ['py', 'html', 'htm'] %} 104 | {{ get_snippet(filepath, line, 10) }} 105 | {% else %} 106 |

    Cannot load file, type not supported.

    107 | {% end %} 108 |
    111 | 112 |

    Full Traceback

    113 |
    114 | {% for filepath, line, method, code in traceback_list %} 115 | 116 | 117 | 120 | 121 | 122 | 123 | 131 | 132 |
    118 |

    on line {{ line }} of {{ method }} in {{ os.path.basename(filepath) }}

    119 |
    File: {{ filepath }}
    124 | {% set extension = os.path.splitext(filepath)[1][1:] %} 125 | {% if extension in ['py', 'html', 'htm'] %} 126 | {{ get_snippet(filepath, line, 10) }} 127 | {% else %} 128 |

    Cannot load file, type not supported.

    129 | {% end %} 130 |
    133 |
    134 | {% end %} 135 |
    136 | 137 |

    Request Headers

    138 |
    139 | 140 | {% for hk, hv in handler.request.headers.iteritems() %} 141 | 142 | 143 | 144 | 145 | {% end %} 146 |
    {{ hk }}{{ hv }}
    147 |
    148 | 149 |

    Response Headers

    150 |
    151 | 152 | {% for hk, hv in handler._headers.iteritems() %} 153 | 154 | 155 | 156 | 157 | {% end %} 158 |
    {{ hk }}{{ hv }}
    159 |
    160 |
    161 | {% end %} 162 | 163 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/links/add.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Add Link") }}{% end %} 3 | {% block content %} 4 |
    5 |

    {{ _("Add Link") }}

    6 | {% from pypress.filters import field_errors %} 7 |
    8 | {{ xsrf_form_html() }} 9 |
    10 | {{ form.name.label() }} 11 | {{ form.name(class_="text") }} 12 | {{ field_errors(form.name) }} 13 |
    14 |
    15 | {{ form.link.label() }} 16 | {{ form.link(class_="text") }} 17 | {{ field_errors(form.link) }} 18 |
    19 |
    20 | {{ form.logo.label() }} 21 | {{ form.logo(class_="text") }} 22 | {{ field_errors(form.logo) }} 23 |
    24 |
    25 | {{ form.email.label() }} 26 | {{ form.email(class_="text") }} 27 | {{ field_errors(form.email) }} 28 |
    29 |
    30 | {{ form.description.label() }} 31 | {{ form.description(style="width: 300px;", class_="text") }} 32 | {{ field_errors(form.description) }} 33 |
    34 |
    {{ form.submit(class_="form-button") }}
    35 |
    36 |
    37 | {% end %} 38 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/links/list.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %}{{ _("Links") }}{% end %} 3 | {% block content %} 4 |

    {{ _("Links") }}

    5 | 31 | {% end %} 32 | 33 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/macros/_field_errors.html: -------------------------------------------------------------------------------- 1 | {% if field.errors %} 2 |
      3 | {% for error in field.errors %} 4 |
    • {{ error }}
    • 5 | {% end %} 6 |
    7 | {% end %} 8 | 9 | -------------------------------------------------------------------------------- /pypress/themes/simple/templates/macros/_page.html: -------------------------------------------------------------------------------- 1 | {% if page_obj.pages > 1 %} 2 | 17 | {% end %} 18 | -------------------------------------------------------------------------------- /pypress/translations/zh_CN/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/translations/zh_CN/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /pypress/translations/zh_CN/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Chinese (China) translations for PROJECT. 2 | # Copyright (C) 2012 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2012. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2012-03-23 17:09+0800\n" 11 | "PO-Revision-Date: 2012-03-23 17:18+0800\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: zh_CN \n" 14 | "Plural-Forms: nplurals=1; plural=0\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.6\n" 19 | 20 | #: pypress/forms.py:27 21 | msgid "You can only use letters, numbers or dashes" 22 | msgstr "" 23 | 24 | #: pypress/forms.py:32 25 | msgid "Username or email" 26 | msgstr "" 27 | 28 | #: pypress/forms.py:34 29 | msgid "You must provide an email or username" 30 | msgstr "" 31 | 32 | #: pypress/forms.py:36 pypress/forms.py:53 pypress/forms.py:89 33 | msgid "Password" 34 | msgstr "" 35 | 36 | #: pypress/forms.py:38 37 | msgid "Remember me" 38 | msgstr "" 39 | 40 | #: pypress/forms.py:42 pypress/themes/default/templates/account/login.html:2 41 | msgid "Login" 42 | msgstr "" 43 | 44 | #: pypress/forms.py:46 45 | msgid "Username" 46 | msgstr "" 47 | 48 | #: pypress/forms.py:47 49 | msgid "Username required" 50 | msgstr "" 51 | 52 | #: pypress/forms.py:50 pypress/forms.py:145 53 | msgid "Nickname" 54 | msgstr "" 55 | 56 | #: pypress/forms.py:51 pypress/forms.py:146 57 | msgid "Nickname required" 58 | msgstr "" 59 | 60 | #: pypress/forms.py:54 61 | msgid "Password required" 62 | msgstr "" 63 | 64 | #: pypress/forms.py:56 pypress/forms.py:95 65 | msgid "Password again" 66 | msgstr "" 67 | 68 | #: pypress/forms.py:58 pypress/forms.py:97 69 | msgid "Passwords don't match" 70 | msgstr "" 71 | 72 | #: pypress/forms.py:60 pypress/forms.py:141 pypress/forms.py:169 73 | msgid "Email" 74 | msgstr "" 75 | 76 | #: pypress/forms.py:61 pypress/forms.py:142 77 | msgid "Email required" 78 | msgstr "" 79 | 80 | #: pypress/forms.py:62 pypress/forms.py:83 pypress/forms.py:143 81 | #: pypress/forms.py:170 82 | msgid "A valid email is required" 83 | msgstr "" 84 | 85 | #: pypress/forms.py:64 86 | msgid "Signup Code" 87 | msgstr "" 88 | 89 | #: pypress/forms.py:68 pypress/themes/default/templates/account/signup.html:2 90 | msgid "Signup" 91 | msgstr "" 92 | 93 | #: pypress/forms.py:73 94 | msgid "This username is taken" 95 | msgstr "" 96 | 97 | #: pypress/forms.py:78 98 | msgid "This email is taken" 99 | msgstr "" 100 | 101 | #: pypress/forms.py:82 102 | msgid "Your email address" 103 | msgstr "" 104 | 105 | #: pypress/forms.py:85 106 | msgid "Find password" 107 | msgstr "" 108 | 109 | #: pypress/forms.py:90 110 | msgid "Password is required" 111 | msgstr "" 112 | 113 | #: pypress/forms.py:92 114 | msgid "New Password" 115 | msgstr "" 116 | 117 | #: pypress/forms.py:93 118 | msgid "New Password is required" 119 | msgstr "" 120 | 121 | #: pypress/forms.py:99 pypress/forms.py:120 pypress/forms.py:178 122 | #: pypress/forms.py:185 123 | msgid "Save" 124 | msgstr "" 125 | 126 | #: pypress/forms.py:103 127 | msgid "Recaptcha" 128 | msgstr "" 129 | 130 | #: pypress/forms.py:105 131 | msgid "Delete" 132 | msgstr "" 133 | 134 | #: pypress/forms.py:109 135 | msgid "Title" 136 | msgstr "" 137 | 138 | #: pypress/forms.py:110 139 | msgid "Title required" 140 | msgstr "" 141 | 142 | #: pypress/forms.py:112 143 | msgid "Slug" 144 | msgstr "" 145 | 146 | #: pypress/forms.py:114 147 | msgid "Content" 148 | msgstr "" 149 | 150 | #: pypress/forms.py:115 151 | msgid "Content required" 152 | msgstr "" 153 | 154 | #: pypress/forms.py:117 pypress/themes/default/templates/base.html:47 155 | msgid "Tags" 156 | msgstr "" 157 | 158 | #: pypress/forms.py:118 159 | msgid "Tags required" 160 | msgstr "" 161 | 162 | #: pypress/forms.py:130 163 | msgid "Slug must be less than 50 characters" 164 | msgstr "" 165 | 166 | #: pypress/forms.py:136 167 | msgid "This slug is taken" 168 | msgstr "" 169 | 170 | #: pypress/forms.py:136 171 | msgid "Slug is required" 172 | msgstr "" 173 | 174 | #: pypress/forms.py:148 175 | msgid "Website" 176 | msgstr "" 177 | 178 | #: pypress/forms.py:150 pypress/forms.py:167 pypress/forms.py:174 179 | msgid "A valid url is required" 180 | msgstr "" 181 | 182 | #: pypress/forms.py:152 183 | msgid "Comment" 184 | msgstr "" 185 | 186 | #: pypress/forms.py:153 187 | msgid "Comment required" 188 | msgstr "" 189 | 190 | #: pypress/forms.py:155 191 | msgid "Captcha" 192 | msgstr "" 193 | 194 | #: pypress/forms.py:156 195 | msgid "Captcha required" 196 | msgstr "" 197 | 198 | #: pypress/forms.py:158 199 | #: pypress/themes/default/templates/blog/add_comment.html:2 200 | msgid "Add comment" 201 | msgstr "" 202 | 203 | #: pypress/forms.py:159 pypress/forms.py:186 204 | msgid "Cancel" 205 | msgstr "" 206 | 207 | #: pypress/forms.py:163 208 | msgid "Site name" 209 | msgstr "" 210 | 211 | #: pypress/forms.py:164 212 | msgid "Site name required" 213 | msgstr "" 214 | 215 | #: pypress/forms.py:166 216 | msgid "link" 217 | msgstr "" 218 | 219 | #: pypress/forms.py:172 220 | msgid "Logo" 221 | msgstr "" 222 | 223 | #: pypress/forms.py:176 224 | msgid "Description" 225 | msgstr "" 226 | 227 | #: pypress/forms.py:182 228 | msgid "HTML" 229 | msgstr "" 230 | 231 | #: pypress/forms.py:183 232 | msgid "HTML required" 233 | msgstr "" 234 | 235 | #: pypress/themes/default/templates/base.html:50 236 | msgid "Archives" 237 | msgstr "" 238 | 239 | #: pypress/themes/default/templates/base.html:53 240 | #: pypress/themes/default/templates/blog/post.html:2 241 | msgid "Post Now" 242 | msgstr "" 243 | 244 | #: pypress/themes/default/templates/base.html:44 245 | msgid "Home" 246 | msgstr "" 247 | 248 | #: pypress/themes/default/templates/base.html:60 249 | msgid "login" 250 | msgstr "" 251 | 252 | #: pypress/themes/default/templates/blog/edit.html:2 253 | msgid "Edit" 254 | msgstr "" 255 | 256 | #: pypress/themes/default/templates/blog/list.html:2 257 | msgid "List" 258 | msgstr "" 259 | 260 | #: pypress/themes/default/templates/blog/module-comment.html:21 261 | msgid "Are you sure you want to delete this comment ?" 262 | msgstr "" 263 | 264 | #: pypress/themes/default/templates/blog/module-comment.html:22 265 | msgid "yes" 266 | msgstr "" 267 | 268 | #: pypress/themes/default/templates/blog/module-comment.html:23 269 | msgid "no" 270 | msgstr "" 271 | 272 | #: pypress/themes/default/templates/blog/view.html:23 273 | msgid "edit" 274 | msgstr "" 275 | 276 | #: pypress/themes/default/templates/blog/view.html:87 277 | msgid "No comments have been posted yet." 278 | msgstr "" 279 | 280 | #: pypress/views/account.py:42 281 | #, python-format 282 | msgid "Welcome back, %s" 283 | msgstr "" 284 | 285 | #: pypress/views/account.py:51 286 | msgid "The username or password you provided are incorrect." 287 | msgstr "" 288 | 289 | #: pypress/views/account.py:106 290 | msgid "Code is not allowed" 291 | msgstr "" 292 | 293 | #: pypress/views/blog.py:195 294 | msgid "Captcha don't match" 295 | msgstr "" 296 | 297 | -------------------------------------------------------------------------------- /pypress/uimodules.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | import tornado.web 5 | 6 | class Post(tornado.web.UIModule): 7 | def render(self, post, show_comments=False): 8 | return self.render_string( 9 | "blog/module-post.html", post=post, show_comments=show_comments) 10 | 11 | class List(tornado.web.UIModule): 12 | def render(self, page_obj, page_url, *args): 13 | return self.render_string( 14 | "blog/module-list.html", page_obj=page_obj, page_url=page_url, *args) 15 | 16 | class Comment(tornado.web.UIModule): 17 | def render(self, comment, form): 18 | return self.render_string("blog/module-comment.html", comment=comment, form=form) 19 | 20 | class Paginate(tornado.web.UIModule): 21 | def render(self, page_obj, page_url, *args): 22 | return self.render_string( 23 | "macros/_page.html", page_obj=page_obj, page_url=page_url, *args) 24 | -------------------------------------------------------------------------------- /pypress/utils/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/utils/.DS_Store -------------------------------------------------------------------------------- /pypress/utils/FacesAndCaps.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laoqiu/pypress-tornado/f06c5eab6b6adb2580df025f91c66460e1bb3b2e/pypress/utils/FacesAndCaps.ttf -------------------------------------------------------------------------------- /pypress/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /pypress/utils/imagelib.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | imageProcess.py 5 | ~~~~~~~~~~~~~ 6 | Recaptcha and Thumbnail 7 | 8 | :copyright: (c) 2010 by Laoqiu. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | import os 13 | import random 14 | import Image, ImageFont, ImageDraw, ImageEnhance 15 | import StringIO 16 | 17 | def Recaptcha(text): 18 | img = Image.new('RGB',size=(110,26),color=(255,255,255)) 19 | 20 | # set font 21 | font = ImageFont.truetype(os.path.join(os.path.dirname(__file__),'FacesAndCaps.ttf'),25) 22 | draw = ImageDraw.Draw(img) 23 | colors = [(250,125,30),(15,65,150),(210,30,90),(64,25,90),(10,120,40),(95,0,16)] 24 | 25 | # write text 26 | for i,s in enumerate(text): 27 | position = (i*25+4,0) 28 | draw.text(position, s, fill=random.choice(colors),font=font) 29 | 30 | # set border 31 | #draw.line([(0,0),(99,0),(99,29),(0,29),(0,0)], fill=(180,180,180)) 32 | del draw 33 | 34 | # push data 35 | strIO = StringIO.StringIO() 36 | img.save(strIO,'PNG') 37 | strIO.seek(0) 38 | return strIO 39 | 40 | def reduce_opacity(im, opacity): 41 | """Returns an image with reduced opacity.""" 42 | assert opacity >= 0 and opacity <= 1 43 | if im.mode != 'RGBA': 44 | im = im.convert('RGBA') 45 | else: 46 | im = im.copy() 47 | alpha = im.split()[3] 48 | alpha = ImageEnhance.Brightness(alpha).enhance(opacity) 49 | im.putalpha(alpha) 50 | return im 51 | 52 | class Thumbnail(object): 53 | """ 54 | t = Thumbnail(path) 55 | t.thumb(size=(100,100),outfile='file/to/name.xx',bg=False,watermark=None) 56 | """ 57 | def __init__(self, path): 58 | self.path = path 59 | try: 60 | self.img = Image.open(self.path) 61 | except IOError: 62 | self.img = None 63 | raise "%s not images" % path 64 | 65 | def thumb(self, size=(100,100), outfile=None, filler=False, watermark=None): 66 | """ 67 | outfile: 'file/to/outfile.xxx' 68 | filler: True|False 69 | watermark: 'file/to/watermark.xxx' 70 | """ 71 | if not self.img: 72 | print 'must be have a image to process' 73 | return 74 | 75 | if not outfile: 76 | outfile = self.path 77 | 78 | #原图复制 79 | part = self.img 80 | part.thumbnail(size, Image.ANTIALIAS) # 按比例缩略 81 | 82 | size = size if filler else part.size # 不补白则正常缩放 83 | w,h = size 84 | 85 | layer = Image.new('RGBA',size,(255,255,255)) # 白色底图 86 | 87 | # 计算粘贴的位置 88 | pw,ph = part.size 89 | left = (h-ph)/2 90 | upper = (w-pw)/2 91 | layer.paste(part,(upper,left)) # 粘贴原图 92 | 93 | # 如果有watermark参数则加水印 94 | if watermark: 95 | part = Image.open(watermark) 96 | part = reduce_opacity(part, 0.3) 97 | # 粘贴到右下角 98 | lw,lh = part.size 99 | position = (w-lw,h-lh) 100 | if layer.mode != 'RGBA': 101 | layer.convert('RGBA') 102 | mark = Image.new('RGBA', layer.size, (0,0,0,0)) 103 | mark.paste(part, position) 104 | layer = Image.composite(mark, layer, mark) 105 | 106 | layer.save(outfile, quality=100) # 保存 107 | return outfile 108 | 109 | 110 | if __name__=='__main__': 111 | t = Thumbnail('pic.jpg') 112 | t.thumb() 113 | -------------------------------------------------------------------------------- /pypress/views/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from .base import ErrorHandler, RequestHandler 3 | -------------------------------------------------------------------------------- /pypress/views/account.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | views: account.py 5 | ~~~~~~~~~~~~~~~~~ 6 | :author: laoqiu.com@gmail.com 7 | """ 8 | from datetime import datetime 9 | import cPickle as pickle 10 | 11 | from pypress.views import RequestHandler 12 | from pypress.database import db 13 | from pypress.models import User, UserCode 14 | from pypress.extensions.routing import route 15 | 16 | @route(r'/login', name='login') 17 | class Login(RequestHandler): 18 | def get(self): 19 | 20 | form = self.forms.LoginForm(next=self.get_args('next')) 21 | 22 | self.render('account/login.html', form=form) 23 | return 24 | 25 | def post(self): 26 | 27 | form = self.forms.LoginForm(self.request.arguments) 28 | 29 | if form.validate(): 30 | user, authenticated = User.query.authenticate(self.get_args('login',''), 31 | self.get_args('password','')) 32 | if user and authenticated: 33 | 34 | user.last_login = datetime.utcnow() # utcnow 35 | db.session.commit() 36 | 37 | # set cookie 38 | self.session['user'] = user 39 | # self.session.set_expires(days=2) 40 | self.session.save() 41 | 42 | # flash 43 | self.flash(self._("Welcome back, %s" % user.username), "success") 44 | 45 | # redirect 46 | next_url = form.next.data 47 | if not next_url: 48 | next_url = '/' 49 | self.redirect(next_url) 50 | return 51 | else: 52 | form.submit.errors.append(self._("The username or password you provided are incorrect.")) 53 | 54 | self.render('account/login.html', form=form) 55 | return 56 | 57 | 58 | @route(r'/logout', name='logout') 59 | class Logout(RequestHandler): 60 | def get(self): 61 | 62 | del self.session["user"] 63 | self.session.save() 64 | 65 | # redirect 66 | next_url = self.get_args('next') 67 | if not next_url: 68 | next_url = '/' 69 | self.redirect(next_url) 70 | return 71 | 72 | 73 | @route(r'/signup', name='signup') 74 | class Signup(RequestHandler): 75 | def get(self): 76 | 77 | form = self.forms.SignupForm(next=self.get_args('next')) 78 | 79 | self.render("account/signup.html", form=form) 80 | return 81 | 82 | def post(self): 83 | 84 | form = self.forms.SignupForm(self.request.arguments) 85 | 86 | if form.validate(): 87 | 88 | code = UserCode.query.filter_by(code=form.code.data).first() 89 | 90 | if code: 91 | user = User(role=code.role) 92 | form.populate_obj(user) 93 | 94 | db.session.add(user) 95 | db.session.delete(code) 96 | db.session.commit() 97 | 98 | self.session['user'] = user 99 | 100 | next_url = form.next.data 101 | 102 | if not next_url: 103 | next_url = self.reverse_url('people', user.username) 104 | 105 | return self.redirect(next_url) 106 | else: 107 | form.code.errors.append(self._("Code is not allowed")) 108 | 109 | self.render("account/signup.html", form=form) 110 | return 111 | 112 | 113 | @route(r'/i18n', name='language') 114 | class Language(RequestHandler): 115 | def get(self): 116 | code = self.get_args('lang','en_US') 117 | self.set_cookie('lang', code) 118 | 119 | next_url = self.get_args('next','/') 120 | self.redirect(next_url) 121 | return 122 | 123 | 124 | -------------------------------------------------------------------------------- /pypress/views/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | views: base.py 5 | ~~~~~~~~~~~ 6 | :author: laoqiu.com@gmail.com 7 | """ 8 | import os 9 | 10 | import logging 11 | import tornado.web 12 | import tornado.locale 13 | import tornado.escape 14 | import tornado.ioloop 15 | 16 | from pygments import highlight 17 | from pygments.lexers import get_lexer_for_filename 18 | from pygments.formatters import HtmlFormatter 19 | 20 | from pypress.database import db 21 | from pypress.models import Comment, Tag, Link 22 | from pypress.extensions.permission import Identity, AnonymousIdentity 23 | from pypress.extensions.cache import cache 24 | from pypress.extensions.sessions import RedisSession, Session 25 | 26 | 27 | class FlashMessageMixIn(object): 28 | """ 29 | Store a message between requests which the user needs to see. 30 | 31 | views 32 | ------- 33 | 34 | self.flash("Welcome back, %s" % username, 'success') 35 | 36 | base.html 37 | ------------ 38 | 39 | {% set messages = handler.get_flashed_messages() %} 40 | {% if messages %} 41 |
    42 | {% for category, msg in messages %} 43 | {{ msg }} 44 | {% end %} 45 |
    46 | {% end %} 47 | """ 48 | def flash(self, message, category='message'): 49 | messages = self.messages() 50 | messages.append((category, message)) 51 | self.set_secure_cookie('flash_messages', tornado.escape.json_encode(messages)) 52 | 53 | def messages(self): 54 | messages = self.get_secure_cookie('flash_messages') 55 | messages = tornado.escape.json_decode(messages) if messages else [] 56 | return messages 57 | 58 | def get_flashed_messages(self): 59 | messages = self.messages() 60 | self.clear_cookie('flash_messages') 61 | return messages 62 | 63 | 64 | class PermissionMixIn(object): 65 | @property 66 | def identity(self): 67 | if not hasattr(self, "_identity"): 68 | self._identity = self.get_identity() 69 | return self._identity 70 | 71 | def get_identity(self): 72 | if self.current_user: 73 | identity = Identity(self.current_user.id) 74 | identity.provides.update(self.current_user.provides) 75 | return identity 76 | return AnonymousIdentity() 77 | 78 | 79 | class CachedItemsMixIn(object): 80 | def get_cached_items(self, name): 81 | items = cache.get(name) 82 | if items is None: 83 | items = self.set_cached_items(name) 84 | return items 85 | 86 | def set_cached_items(self, name, limit=10): 87 | items = [] 88 | if name == 'latest_comments': 89 | items = [comment.item for comment in Comment.query.order_by(Comment.created_date.desc()).limit(limit)] 90 | elif name == 'tags': 91 | items = Tag.query.cloud() 92 | elif name == 'links': 93 | items = [link.item for link in Link.query.filter(Link.passed==True).limit(limit)] 94 | cache.set(name, items) 95 | return items 96 | 97 | 98 | class RequestHandler(tornado.web.RequestHandler, PermissionMixIn, FlashMessageMixIn, CachedItemsMixIn): 99 | def on_finish(self): 100 | """sqlalchemy connection close. 101 | fixed sqlalchemy error: 'Can't reconnect until invalid'. new in version 2.2""" 102 | db.session.remove() 103 | 104 | def get_current_user(self): 105 | user = self.session['user'] if 'user' in self.session else None 106 | return user 107 | 108 | @property 109 | def session(self): 110 | if hasattr(self, '_session'): 111 | return self._session 112 | else: 113 | self.require_setting('permanent_session_lifetime', 'session') 114 | expires = self.settings['permanent_session_lifetime'] or None 115 | if 'redis_server' in self.settings and self.settings['redis_server']: 116 | sessionid = self.get_secure_cookie('sid') 117 | self._session = RedisSession(self.application.session_store, sessionid, expires_days=expires) 118 | if not sessionid: 119 | self.set_secure_cookie('sid', self._session.id, expires_days=expires) 120 | else: 121 | self._session = Session(self.get_secure_cookie, self.set_secure_cookie, expires_days=expires) 122 | return self._session 123 | 124 | def get_user_locale(self): 125 | code = self.get_cookie('lang', self.settings.get('default_locale', 'zh_CN')) 126 | return tornado.locale.get(code) 127 | 128 | def get_template_path(self): 129 | if 'theme_template_path' in self.settings: 130 | return self.settings['theme_template_path'] 131 | return self.settings.get('template_path') 132 | 133 | def get_error_html(self, status_code, **kwargs): 134 | if self.settings.get('debug', False) is False: 135 | self.set_status(status_code) 136 | return self.render_string('errors/%s.html' % status_code) 137 | 138 | else: 139 | def get_snippet(fp, target_line, num_lines): 140 | if fp.endswith('.html'): 141 | fp = os.path.join(self.get_template_path(), fp) 142 | 143 | half_lines = (num_lines/2) 144 | try: 145 | with open(fp) as f: 146 | all_lines = [line for line in f] 147 | code = ''.join(all_lines[target_line-half_lines-1:target_line+half_lines]) 148 | formatter = HtmlFormatter(linenos=True, linenostart=target_line-half_lines, hl_lines=[half_lines+1]) 149 | lexer = get_lexer_for_filename(fp) 150 | return highlight(code, lexer, formatter) 151 | 152 | except Exception, ex: 153 | logging.error(ex) 154 | return '' 155 | 156 | css = HtmlFormatter().get_style_defs('.highlight') 157 | exception = kwargs.get('exception', None) 158 | return self.render_string('errors/exception.htm', 159 | get_snippet=get_snippet, 160 | css=css, 161 | exception=exception, 162 | status_code=status_code, 163 | kwargs=kwargs) 164 | 165 | def get_args(self, key, default=None, type=None): 166 | if type==list: 167 | if default is None: default = [] 168 | return self.get_arguments(key, default) 169 | value = self.get_argument(key, default) 170 | if value and type: 171 | try: 172 | value = type(value) 173 | except ValueError: 174 | value = default 175 | return value 176 | 177 | @property 178 | def is_xhr(self): 179 | '''True if the request was triggered via a JavaScript XMLHttpRequest. 180 | This only works with libraries that support the `X-Requested-With` 181 | header and set it to "XMLHttpRequest". Libraries that do that are 182 | prototype, jQuery and Mochikit and probably some more.''' 183 | return self.request.headers.get('X-Requested-With', '') \ 184 | .lower() == 'xmlhttprequest' 185 | 186 | @property 187 | def forms(self): 188 | return self.application.forms[self.locale.code] 189 | 190 | def _(self, message, plural_message=None, count=None): 191 | return self.locale.translate(message, plural_message, count) 192 | 193 | 194 | class ErrorHandler(RequestHandler): 195 | """raise 404 error if url is not found. 196 | fixed tornado.web.RequestHandler HTTPError bug. 197 | """ 198 | def prepare(self): 199 | self.set_status(404) 200 | raise tornado.web.HTTPError(404) 201 | 202 | 203 | -------------------------------------------------------------------------------- /pypress/views/blog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | views: blog.py 5 | ~~~~~~~~~~~~~~~~~ 6 | :author: laoqiu.com@gmail.com 7 | """ 8 | import json 9 | import os 10 | import urllib 11 | import logging 12 | import tornado.web 13 | import tornado.escape 14 | 15 | from datetime import datetime 16 | 17 | from pypress.views import RequestHandler 18 | from pypress.database import db 19 | from pypress.models import User, Post, Tag, Comment 20 | from pypress.helpers import generate_random 21 | from pypress.utils.imagelib import Recaptcha 22 | from pypress.extensions.routing import route 23 | 24 | 25 | @route(r'/', name='archive') 26 | @route(r'/(\d{4})/', name='archive_year') 27 | @route(r'/(\d{4})/(\d{1,2})/', name='archive_month') 28 | @route(r'/(\d{4})/(\d{1,2})/(\d{1,2})/', name='archive_day') 29 | class Archive(RequestHandler): 30 | def get(self, year=None, month=None, day=None): 31 | 32 | page = self.get_args('page', 1, type=int) 33 | 34 | page_obj = Post.query.archive(year, month, day).as_list() \ 35 | .paginate(page=page, per_page=Post.PER_PAGE) 36 | 37 | if day: 38 | path = self.reverse_url('archive_day', year, month, day) 39 | elif month: 40 | path = self.reverse_url('archive_month', year, month) 41 | elif year: 42 | path = self.reverse_url('archive_year', year) 43 | else: 44 | path = self.reverse_url('archive') 45 | 46 | page_url = lambda page: path + \ 47 | '?%s' % urllib.urlencode(dict(page=page)) 48 | 49 | self.render("blog/list.html", 50 | page_obj=page_obj, 51 | page_url=page_url) 52 | return 53 | 54 | 55 | @route(r'/archives', name='archives') 56 | class Archives(RequestHandler): 57 | def get(self): 58 | 59 | page = self.get_args('page', 1, type=int) 60 | 61 | page_obj = Post.query.as_list() \ 62 | .paginate(page=page, per_page=Post.PER_PAGE) 63 | 64 | page_url = lambda page: self.reverse_url('archives') + \ 65 | '?%s' % urllib.urlencode(dict(page=page)) 66 | 67 | self.render("blog/archives.html", 68 | page_obj=page_obj, 69 | page_url=page_url) 70 | return 71 | 72 | 73 | @route(r'/search', name='search') 74 | class Search(RequestHandler): 75 | def get(self): 76 | 77 | keywords = self.get_args('q') 78 | page = self.get_args('page', 1, type=int) 79 | 80 | if not keywords: 81 | self.redirect('/') 82 | return 83 | 84 | page_obj = Post.query.search(keywords).as_list() \ 85 | .paginate(page, per_page=Post.PER_PAGE) 86 | 87 | if page_obj.total == 1: 88 | post = page_obj.items[0] 89 | self.redirect(post.url) 90 | return 91 | 92 | page_url = lambda page: self.reverse_url('search') + \ 93 | '?%s' % urllib.urlencode(dict(page=page, 94 | q=keywords)) 95 | 96 | self.render("blog/search.html", 97 | page_obj=page_obj, 98 | page_url=page_url, 99 | keywords=keywords) 100 | return 101 | 102 | 103 | @route(r'/tags', name='tags') 104 | class TagWall(RequestHandler): 105 | def get(self): 106 | 107 | tags = Tag.query.cloud() 108 | 109 | self.render("blog/tags.html", tags=tags) 110 | return 111 | 112 | 113 | @route(r'/tag/(.+)', name='tag') 114 | class TagView(RequestHandler): 115 | def get(self, slug): 116 | 117 | page = self.get_args('page', 1, type=int) 118 | 119 | tag = Tag.query.filter_by(slug=slug).first_or_404() 120 | 121 | page_obj = tag.posts.as_list() \ 122 | .paginate(page, per_page=Post.PER_PAGE) 123 | 124 | page_url = lambda page: self.reverse_url('tag', slug) + \ 125 | '?%s' % urllib.urlencode(dict(page=page)) 126 | 127 | self.render("blog/tag.html", 128 | page_obj=page_obj, 129 | page_url=page_url, 130 | tagname=tag.name) 131 | return 132 | 133 | 134 | @route(r'/people/(.+)', name='people') 135 | class People(RequestHandler): 136 | def get(self, username): 137 | 138 | page = self.get_args('page', 1, type=int) 139 | 140 | people = User.query.get_by_username(username) 141 | 142 | page_obj = Post.query.filter(Post.author_id==people.id).as_list() \ 143 | .paginate(page, per_page=Post.PER_PAGE) 144 | 145 | page_url = lambda page: self.reverse_url('people', username) + \ 146 | '?%s' % urllib.urlencode(dict(page=page)) 147 | 148 | self.render("blog/people.html", 149 | page_obj=page_obj, 150 | page_url=page_url, 151 | people=people) 152 | return 153 | 154 | 155 | @route(r'/(\d{4})/(\d{1,2})/(\d{1,2})/(.+)', name='post_view') 156 | class View(RequestHandler): 157 | def get(self, year, month, day, slug): 158 | 159 | post = Post.query.get_by_slug(slug) 160 | 161 | date = (post.created_date.year, 162 | post.created_date.month, 163 | post.created_date.day) 164 | 165 | if (int(year), int(month), int(day)) != date: 166 | raise tornado.web.HTTPError(404) 167 | 168 | self.render("blog/view.html", post=post, form=self.forms.CommentForm()) 169 | return 170 | 171 | def post(self, year, month, day, slug): 172 | """ add comment """ 173 | 174 | post = Post.query.get_by_slug(slug) 175 | 176 | form = self.forms.CommentForm(self.request.arguments) 177 | 178 | if form.validate(): 179 | 180 | captcha = form.captcha.data 181 | 182 | if self.get_secure_cookie("captcha") == captcha: 183 | 184 | comment = Comment(post=post, 185 | ip=self.request.remote_ip) 186 | 187 | form.populate_obj(comment) 188 | 189 | if self.current_user: 190 | comment.author_id = self.current_user.id 191 | 192 | db.session.add(comment) 193 | db.session.commit() 194 | 195 | self.redirect(comment.url) 196 | return 197 | 198 | form.captcha.errors.append(self._("Captcha don't match")) 199 | 200 | self.render("blog/view.html", post=post, form=form) 201 | return 202 | 203 | 204 | @route(r'/post', name='post') 205 | class Submit(RequestHandler): 206 | @tornado.web.authenticated 207 | def get(self): 208 | 209 | form = self.forms.PostForm(next=self.get_args('next','')) 210 | 211 | self.render("blog/post.html", form=form) 212 | return 213 | 214 | @tornado.web.authenticated 215 | def post(self): 216 | 217 | form = self.forms.PostForm(self.request.arguments) 218 | 219 | if form.validate(): 220 | 221 | post = Post(author_id=self.current_user.id) 222 | form.populate_obj(post) 223 | 224 | db.session.add(post) 225 | db.session.commit() 226 | 227 | # redirect 228 | next_url = form.next.data 229 | if not next_url: 230 | next_url = post.url 231 | self.redirect(next_url) 232 | return 233 | 234 | self.render("blog/post.html", form=form) 235 | return 236 | 237 | 238 | @route(r'/post/(\d+)/edit', name='post_edit') 239 | class Edit(RequestHandler): 240 | @tornado.web.authenticated 241 | def get(self, post_id): 242 | 243 | post = Post.query.get_or_404(post_id) 244 | 245 | post.permissions.edit.test(self.identity, 401) 246 | 247 | form = self.forms.PostForm(title = post.title, 248 | slug = post.slug, 249 | content = post.content, 250 | tags = post.tags, 251 | obj = post) 252 | 253 | self.render("blog/edit.html", form=form) 254 | return 255 | 256 | @tornado.web.authenticated 257 | def post(self, post_id): 258 | 259 | post = Post.query.get_or_404(post_id) 260 | 261 | post.permissions.edit.test(self.identity, 401) 262 | 263 | form = self.forms.PostForm(self.request.arguments, obj=post) 264 | 265 | if form.validate(): 266 | 267 | form.populate_obj(post) 268 | db.session.commit() 269 | 270 | next_url = post.url 271 | self.redirect(next_url) 272 | return 273 | 274 | self.render("blog/edit.html", form=form) 275 | return 276 | 277 | 278 | @route(r'/post/(\d+)/delete', name='post_delete') 279 | class Delete(RequestHandler): 280 | @tornado.web.authenticated 281 | def get(self, post_id): 282 | 283 | post = Post.query.get_or_404(post_id) 284 | 285 | post.permissions.delete.test(self.identity, 401) 286 | 287 | db.session.delete(post) 288 | db.session.commit() 289 | 290 | self.redirect('/') 291 | return 292 | 293 | 294 | @route(r'/comment/(\d+)/delete', name='comment_delete') 295 | class DeleteComment(RequestHandler): 296 | @tornado.web.authenticated 297 | def post(self, comment_id): 298 | 299 | comment = Comment.query.get_or_404(int(comment_id)) 300 | 301 | comment.permissions.delete.test(self.identity, 401) 302 | 303 | db.session.delete(comment) 304 | db.session.commit() 305 | 306 | self.write(dict(success=True, 307 | comment_id=comment_id)) 308 | return 309 | 310 | 311 | @route(r'/upload', name='upload') 312 | class Upload(RequestHandler): 313 | def post(self): 314 | if 'upload' in self.request.files: 315 | f = self.request.files['upload'][0] 316 | 317 | if 'image' in f['content_type']: 318 | now = datetime.now() 319 | alt, ext = os.path.splitext(f['filename']) 320 | filename = now.strftime('%d%H%M%S%f') + ext 321 | dirs = os.path.join(self.settings['upload_path'], str(now.year), str(now.month)) 322 | if not os.path.isdir(dirs): 323 | os.makedirs(dirs) 324 | path = os.path.join(dirs, filename) 325 | try: 326 | outfile = open(path, 'w') 327 | outfile.write(f['body']) 328 | outfile.close() 329 | except: 330 | logging.debug("upload error") 331 | error = "save file error" 332 | else: 333 | filename = os.path.join('/upload', str(now.year), str(now.month), filename) 334 | self.write(json.dumps(dict(success=True, result=dict(path=filename,alt=alt)))) 335 | # self.write(dict(success=True, result=dict(path=filename,alt=alt))) 336 | return 337 | else: 338 | error = "file not image" 339 | else: 340 | error = "upload not in request.files" 341 | 342 | self.write(dict(success=False, error=error)) 343 | return 344 | 345 | 346 | @route(r'/captcha/get', name='get_captcha') 347 | class GetCaptcha(RequestHandler): 348 | def get(self): 349 | text = generate_random(4) 350 | self.set_secure_cookie("captcha", text) 351 | 352 | strIO = Recaptcha(text) 353 | 354 | #,mimetype='image/png' 355 | self.set_header("Content-Type", "image/png") 356 | self.write(strIO.read()) 357 | return 358 | 359 | 360 | @route(r'/captcha/check', name='check_captcha') 361 | class CheckCaptcha(RequestHandler): 362 | def get(self): 363 | 364 | captcha = self.get_args('captcha') 365 | 366 | if self.get_secure_cookie("captcha") == captcha: 367 | success = True 368 | else: 369 | success = False 370 | 371 | self.write(dict(success=success)) 372 | return 373 | 374 | 375 | 376 | -------------------------------------------------------------------------------- /pypress/views/links.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | views: links.py 5 | ~~~~~~~~~~~~~~~~~ 6 | :author: laoqiu.com@gmail.com 7 | """ 8 | import urllib 9 | import tornado.web 10 | 11 | from pypress.views import RequestHandler 12 | from pypress.database import db 13 | from pypress.models import Link 14 | from pypress.extensions.routing import route 15 | from pypress.permissions import admin 16 | 17 | 18 | @route(r'/links', name='links') 19 | class Links(RequestHandler): 20 | def get(self): 21 | 22 | page = self.get_args('page', 1, type=int) 23 | 24 | page_obj = Link.query.paginate(page=page, per_page=Link.PER_PAGE) 25 | 26 | page_url = lambda page: self.reverse_url('links') + \ 27 | '?%s' % urllib.urlencode(dict(page=page)) 28 | 29 | self.render("links/list.html", 30 | page_obj=page_obj, 31 | page_url=page_url) 32 | return 33 | 34 | 35 | @route(r'/link/add', name='link_add') 36 | class Add(RequestHandler): 37 | def get(self): 38 | 39 | form = self.forms.LinkForm() 40 | 41 | self.render("links/add.html", form=form) 42 | return 43 | 44 | def post(self): 45 | 46 | form = self.forms.LinkForm(self.request.arguments) 47 | 48 | if form.validate(): 49 | 50 | link = Link() 51 | form.populate_obj(link) 52 | 53 | db.session.add(link) 54 | db.session.commit() 55 | 56 | self.flash("Waiting for passed...") 57 | 58 | next_url = self.get_args('next', self.reverse_url('links')) 59 | self.redirect(next_url) 60 | 61 | self.render("links/add.html", form=form) 62 | return 63 | 64 | 65 | @route(r'/link/(\d+)/pass', name='link_pass') 66 | class Pass(RequestHandler): 67 | @tornado.web.authenticated 68 | @admin.require(401) 69 | def get(self, id): 70 | 71 | link = Link.query.get_or_404(int(id)) 72 | 73 | link.passed = True 74 | 75 | next_url = self.get_args('next', self.reverse_url('links')) 76 | self.redirect(next_url) 77 | return 78 | 79 | 80 | @route(r'/link/(\d+)/delete', name='link_delete') 81 | class Delete(RequestHandler): 82 | @tornado.web.authenticated 83 | @admin.require(401) 84 | def get(self, id): 85 | 86 | link = Link.query.get_or_404(int(id)) 87 | 88 | db.session.delete(link) 89 | db.session.commit() 90 | 91 | next_url = self.get_args('next', self.reverse_url('links')) 92 | self.redirect(next_url) 93 | return 94 | 95 | 96 | --------------------------------------------------------------------------------