├── pypress ├── logs │ ├── debug.log │ └── error.log ├── templates │ ├── blog │ │ ├── comment.html │ │ ├── _postnow.html │ │ ├── _archive.html │ │ ├── _search.html │ │ ├── _tags.html │ │ ├── _comment.html │ │ ├── _links.html │ │ ├── tags.html │ │ ├── archive.html │ │ ├── template_edit.html │ │ ├── search_result.html │ │ ├── about.html │ │ ├── links.html │ │ ├── add_link.html │ │ ├── add_comment.html │ │ ├── list.html │ │ ├── people.html │ │ ├── view.html │ │ └── submit.html │ ├── errors │ │ ├── 500.html │ │ ├── 404.html │ │ └── 403.html │ ├── macros │ │ ├── _forms.html │ │ ├── _page.html │ │ ├── _twitter.html │ │ └── _post.html │ └── account │ │ ├── login.html │ │ └── signup.html ├── static │ ├── favicon.ico │ └── js │ │ ├── public.js │ │ └── jquery.idTabs.min.js ├── models │ ├── __init__.py │ ├── types.py │ ├── users.py │ └── blog.py ├── themes │ └── default │ │ ├── static │ │ ├── comment.gif │ │ ├── bg_button.png │ │ ├── feed-14x14.png │ │ ├── highlight.css │ │ └── style.css │ │ ├── info.json │ │ └── templates │ │ └── layout.html ├── translations │ └── zh │ │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po ├── views │ ├── __init__.py │ ├── comment.py │ ├── feeds.py │ ├── link.py │ ├── post.py │ ├── account.py │ └── frontend.py ├── forms │ ├── __init__.py │ ├── validators.py │ ├── blog.py │ └── account.py ├── signals.py ├── extensions.py ├── permissions.py ├── config.cfg ├── __init__.py └── helpers.py ├── babel.cfg ├── .gitignore ├── fcgi.py ├── requirements.txt ├── README.md ├── manage.py └── messages.pot /pypress/logs/debug.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pypress/logs/error.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pypress/templates/blog/comment.html: -------------------------------------------------------------------------------- 1 | Your comment javascript code... 2 | -------------------------------------------------------------------------------- /pypress/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UlricQin/pypress/master/pypress/static/favicon.ico -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | [jinja2: **/templates/**.html] 3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 4 | -------------------------------------------------------------------------------- /pypress/models/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python from .users import User, UserCode, Twitter from .blog import Post, Tag, Comment, Link -------------------------------------------------------------------------------- /pypress/themes/default/static/comment.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UlricQin/pypress/master/pypress/themes/default/static/comment.gif -------------------------------------------------------------------------------- /pypress/themes/default/static/bg_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UlricQin/pypress/master/pypress/themes/default/static/bg_button.png -------------------------------------------------------------------------------- /pypress/themes/default/static/feed-14x14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UlricQin/pypress/master/pypress/themes/default/static/feed-14x14.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.pyo 4 | *.egg-info 5 | *.bak 6 | *.sql 7 | *.pid 8 | *.db 9 | *.swp 10 | # virtualenv 11 | env 12 | -------------------------------------------------------------------------------- /pypress/translations/zh/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UlricQin/pypress/master/pypress/translations/zh/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /pypress/views/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python from .frontend import frontend from .post import post from .account import account from .comment import comment from .link import link from .feeds import feeds -------------------------------------------------------------------------------- /fcgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from pypress import create_app 3 | 4 | app = create_app('config.cfg') 5 | 6 | from flup.server.fcgi import WSGIServer 7 | WSGIServer(app,bindAddress='/tmp/pypress.sock').run() 8 | -------------------------------------------------------------------------------- /pypress/templates/blog/_postnow.html: -------------------------------------------------------------------------------- 1 | {% if g.user %} 2 |
3 | {% endif %} 4 | -------------------------------------------------------------------------------- /pypress/forms/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python from .account import LoginForm, SignupForm, RecoverPasswordForm, \ ChangePasswordForm, DeleteAccountForm, TwitterForm from .blog import PostForm, CommentForm, LinkForm, TemplateForm -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-OAuth 3 | Flask-Cache 4 | Flask-SQLAlchemy 5 | Flask-Principal 6 | Flask-WTF 7 | Flask-Mail 8 | Flask-Script 9 | Flask-Babel 10 | Flask-Themes 11 | Flask-Uploads 12 | pygments 13 | markdown 14 | blinker 15 | -------------------------------------------------------------------------------- /pypress/signals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | from blinker import Namespace 5 | 6 | signals = Namespace() 7 | 8 | comment_added = signals.signal("comment-added") 9 | comment_deleted = signals.signal("comment-deleted") 10 | 11 | -------------------------------------------------------------------------------- /pypress/themes/default/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "application": "pypress", 3 | "identifier": "default", 4 | "name": "default", 5 | "author": "LaoQiu", 6 | "description": "theme for pypress", 7 | "license": "MIT/X11", 8 | "doctype": "html5" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /pypress/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% block content %} 4 |
5 |

{{ _("500, An Error Has Occurred") }}

6 |
7 |

The requested page,

8 |

an error has occurred —

9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /pypress/extensions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | from flaskext.mail import Mail 5 | from flaskext.sqlalchemy import SQLAlchemy 6 | from flaskext.cache import Cache 7 | from flaskext.uploads import UploadSet, IMAGES 8 | 9 | __all__ = ['mail', 'db', 'cache', 'photos'] 10 | 11 | mail = Mail() 12 | db = SQLAlchemy() 13 | cache = Cache() 14 | photos = UploadSet('photos', IMAGES) 15 | 16 | -------------------------------------------------------------------------------- /pypress/permissions.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | #coding=utf-8 3 | from flaskext.principal import RoleNeed, Permission 4 | 5 | admin = Permission(RoleNeed('admin')) 6 | moderator = Permission(RoleNeed('moderator')) 7 | auth = Permission(RoleNeed('authenticated')) 8 | 9 | # this is assigned when you want to block a permission to all 10 | # never assign this role to anyone ! 11 | null = Permission(RoleNeed('null')) 12 | -------------------------------------------------------------------------------- /pypress/templates/macros/_forms.html: -------------------------------------------------------------------------------- 1 | {% macro render_errors(field) %} 2 | {% if field.errors %} 3 | 8 | {% endif %} 9 | {% endmacro %} 10 | 11 | {% macro render_tips(field) %} 12 | {% if field.description %} 13 | {{ field.description }} 14 | {% endif %} 15 | {% endmacro %} 16 | -------------------------------------------------------------------------------- /pypress/forms/validators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | """ 5 | validators.py 6 | ~~~~~~~~~~~~~ 7 | 8 | :license: BSD, see LICENSE for more details. 9 | """ 10 | from flaskext.wtf import regexp 11 | 12 | from flaskext.babel import lazy_gettext as _ 13 | 14 | USERNAME_RE = r'^[\w.+-]+$' 15 | 16 | is_username = regexp(USERNAME_RE, 17 | message=_("You can only use letters, numbers or dashes")) 18 | 19 | -------------------------------------------------------------------------------- /pypress/templates/blog/_archive.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /pypress/templates/blog/_search.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /pypress/templates/blog/_tags.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /pypress/templates/blog/_comment.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /pypress/templates/macros/_page.html: -------------------------------------------------------------------------------- 1 | {% macro paginate(page_obj, page_url) %} 2 | {% if page_obj.pages > 1 %} 3 | 18 | {% endif %} 19 | {% endmacro %} 20 | -------------------------------------------------------------------------------- /pypress/templates/macros/_twitter.html: -------------------------------------------------------------------------------- 1 | {% macro tweet_box(tweets) %} 2 | {%- if tweets %} 3 |
4 | 13 |
14 | {%- else %} 15 |

No Tweets yet.

16 | {%- endif %} 17 | {% endmacro %} 18 | -------------------------------------------------------------------------------- /pypress/templates/blog/_links.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /pypress/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% block content %} 4 |
5 |

{{ _("404, Page Not Found") }}

6 |
7 |

The requested page,

8 |

cannot be found —

9 |

We're sorry.

10 |
11 |
12 |

{{ _("Maybe you can try search:") }}

13 |
14 | 15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /pypress/templates/errors/403.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% block content %} 4 |
5 |

{{ _("403, Page Not Allowed") }}

6 |
7 |

The requested page,

8 |

cannot be allowed —

9 |

Your permission is not enough

10 |
11 |
12 |

{{ _("Maybe we can help you out though:") }}

13 | 17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A team blog based on [Flask](http://flask.pocoo.org/) 2 | === 3 | 4 | This project isn't supported at the moment, please see a newer [pypress-tornado](https://github.com/laoqiu/pypress-tornado) 5 | 6 | Thanks for flask_website and newsmeme at [http://flask.pocoo.org/community/poweredby/] 7 | 8 | ##Install 9 | 10 | ###Prerequisite 11 | 12 | pip install -r requirements.txt 13 | 14 | ###Custom the Configuration 15 | 16 | pypress/config.cfg 17 | 18 | ###Sync database 19 | 20 | python manage.py createall 21 | 22 | ###Run 23 | 24 | python manage.py runserver 25 | 26 | ##Example 27 | ###Create Users 28 | 29 | Admin: 30 | 31 | python manage.py createcode -r admin 32 | 33 | Create three members in a batch: 34 | 35 | python manage.py createcode -r member -n 3 36 | 37 | ###Signup 38 | 39 | http://localhost:8080/account/signup/ 40 | -------------------------------------------------------------------------------- /pypress/templates/blog/tags.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% from "macros/_page.html" import paginate %} 4 | 5 | {%- block content %} 6 |
7 |

{{ _("Tags") }}

8 |
9 | 14 |
15 |
16 | {%- endblock %} 17 | {%- block sidebar %} 18 | 26 | {%- endblock %} 27 | 28 | -------------------------------------------------------------------------------- /pypress/templates/blog/archive.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% from "macros/_page.html" import paginate %} 4 | 5 | {%- block content %} 6 |
7 |

{{ _("Archive") }}

8 |
9 | 14 |
15 |
16 | {%- endblock %} 17 | {%- block sidebar %} 18 | 26 | {%- endblock %} 27 | -------------------------------------------------------------------------------- /pypress/config.cfg: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | SECRET_KEY = 'secret test' 3 | 4 | SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db' 5 | SQLALCHEMY_ECHO = False 6 | 7 | UPLOADS_DEFAULT_DEST = '/path/to/pypress/static/' 8 | UPLOADS_DEFAULT_URL = '/static' 9 | 10 | CACHE_TYPE = "simple" 11 | CACHE_DEFAULT_TIMEOUT = 300 12 | 13 | THEME = 'default' 14 | 15 | USE_LOCAL_COMMENT = True # if false, to include comment.html 16 | 17 | ACCEPT_LANGUAGES = ['en', 'zh'] 18 | 19 | BABEL_DEFAULT_LOCALE = 'zh' 20 | BABEL_DEFAULT_TIMEZONE = 'Asia/Shanghai' 21 | 22 | # http://twitter.com/oauth_clients/new 23 | TWITTER_KEY = '' 24 | TWITTER_SECRET = '' 25 | 26 | PER_PAGE = 20 27 | 28 | DEBUG_LOG = 'logs/debug.log' 29 | ERROR_LOG = 'logs/error.log' 30 | 31 | ADMINS = ('yourname@domain.com',) 32 | 33 | MAIL_SERVER = 'smtp.gmail.com' 34 | MAIL_PORT = 465 35 | MAIL_USE_TLS = False 36 | MAIL_USE_SSL = True 37 | MAIL_DEBUG = DEBUG 38 | MAIL_USERNAME = 'username' 39 | MAIL_PASSWORD = 'password' 40 | DEFAULT_MAIL_SENDER = 'yourname@domain.com' 41 | -------------------------------------------------------------------------------- /pypress/templates/blog/template_edit.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% from "macros/_forms.html" import render_errors %} 4 | 5 | {%- block content %} 6 |
7 |

{{ _("Edit template") }}

8 |
9 | {{ form.hidden_tag() }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
Path{{ path }}
{{ form.html.label }}{{ form.html(class="text", style="width:600px;height:300px;") }} {{ render_errors(form.html) }}
{{ form.submit(class="button") }}
26 |
27 |
28 | {%- endblock %} 29 | {%- block sidebar %}{%- endblock %} 30 | 31 | -------------------------------------------------------------------------------- /pypress/views/comment.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | views: post.py 5 | ~~~~~~~~~~~~~ 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | from flask import Module, Response, request, flash, jsonify, g, current_app,\ 10 | abort, redirect, url_for, session, send_file, send_from_directory 11 | 12 | from flaskext.babel import gettext as _ 13 | 14 | from pypress import signals 15 | from pypress.helpers import render_template, cached 16 | from pypress.permissions import auth 17 | from pypress.extensions import db 18 | from pypress.models import Comment 19 | 20 | comment = Module(__name__) 21 | 22 | @comment.route("//delete/", methods=("POST",)) 23 | @auth.require(401) 24 | def delete(comment_id): 25 | 26 | comment = Comment.query.get_or_404(comment_id) 27 | comment.permissions.delete.test(403) 28 | 29 | db.session.delete(comment) 30 | db.session.commit() 31 | 32 | signals.comment_deleted.send(comment.post) 33 | 34 | return jsonify(success=True, 35 | comment_id=comment_id) 36 | 37 | 38 | -------------------------------------------------------------------------------- /pypress/templates/blog/search_result.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% from "macros/_page.html" import paginate %} 4 | 5 | {%- block content %} 6 |
7 | {%- if page_obj.items %} 8 |

{{ _("Search results for") }} {{ request.args.get('q','') }}

9 | {%- for post in page_obj.items %} 10 |
11 |

{{ post.title }}

12 | 16 |
17 | {%- endfor %} 18 | {{ paginate(page_obj, page_url) }} 19 | {%- else %} 20 |

{{ _("No posts found. Try a different search.") }}

21 | {%- endif %} 22 |
23 | {%- endblock %} 24 | 25 | -------------------------------------------------------------------------------- /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/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | {% from "macros/_forms.html" import render_errors %} 3 | 4 | {%- block content %} 5 |

{{ _('Login to pypress') }}

6 |
7 |
8 | {{ form.hidden_tag() }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
{{ form.login.label }}{{ form.login(size=30) }} {{ render_errors(form.login) }}
{{ form.password.label }}{{ form.password(size=30) }} {{ render_errors(form.password) }}
{{ form.remember.label }}{{ form.remember }}
{{ form.submit }}
29 |
30 |
31 |

{{ _('Not a member yet ? Sign up !') }}

32 | {%- endblock %} 33 | {%- block sidebar %}{%- endblock %} 34 | -------------------------------------------------------------------------------- /pypress/static/js/public.js: -------------------------------------------------------------------------------- 1 | var ajax_post = function(url, params, on_success){ 2 | var _callback = function(response){ 3 | if (response.success) { 4 | if (response.redirect_url){ 5 | window.location.href = response.redirect_url; 6 | } else if (response.reload){ 7 | window.location.reload(); 8 | } else if (on_success) { 9 | return on_success(response); 10 | } 11 | } else { 12 | return message(response.error, "error"); 13 | } 14 | } 15 | 16 | $.post(url, params, _callback, "json"); 17 | 18 | } 19 | 20 | var message = function(message, category){ 21 | $('ul#messages').html('
  • ' + message + '
  • ').fadeOut(); 22 | } 23 | 24 | var delete_comment = function(url) { 25 | var callback = function(response){ 26 | $('#comment-' + response.comment_id).fadeOut(); 27 | } 28 | ajax_post(url, null, callback); 29 | } 30 | 31 | var delete_link = function(url) { 32 | var callback = function(response){ 33 | $('#link-' + response.link_id).fadeOut(); 34 | } 35 | ajax_post(url, null, callback); 36 | } 37 | 38 | var pass_link = function(url) { 39 | var callback = function(response){ 40 | $('#link-' + response.link_id).find('.link-edit').remove(); 41 | } 42 | ajax_post(url, null, callback); 43 | } 44 | 45 | var hide_flash = function(){ 46 | $("#flashed").fadeOut(); 47 | } 48 | 49 | $(function(){ 50 | setTimeout(hide_flash, 3000); 51 | }) 52 | -------------------------------------------------------------------------------- /pypress/templates/blog/about.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% block content %} 4 |
    5 |

    {{ _("About") }}

    6 |
    7 |

    我在2011年2月14日开始写这个blog系统, 基于flask.

    8 |

    flask跟django, pylons, turboGears, uliweb, bottle, web.py等一样, 同为python的web框架之一. 虽然之前用过上面讲的大部分框架, 但最后选择了flask来写这个blog系统, 原因是它很灵活, 有一个比较稳定的核心(Werkzeug), 更新够快, 据说是作者精力比较旺盛. :)

    9 |

    功能特点:

    10 |
      11 |
    • 支持多语言(i18n)
    • 12 |
    • 支持代码高亮(pygments)
    • 13 |
    • 使用淘宝(kissy-editor)编辑器
    • 14 |
    • 支持多用户
    • 15 |
    • 支持本地或外部(如Disqus)评论
    • 16 |
    • 支持评论级联回复
    • 17 |
    • 不支持分类, 只保留标签功能
    • 18 |
    • 支持模板在线管理
    • 19 |
    • twitter API支持, 可在线发推
    • 20 |
    • RSS支持
    • 21 |
    22 |

    待开发:

    23 |
      24 |
    • 模板在线管理的完善
    • 25 |
    • 高级皮肤制作
    • 26 |
    27 |

    日志:

    28 |
      29 |
    • 1.1.0 2010-04-21 (添加“最新评论”,修复非mysql引擎下的错误)
    • 30 |
    • 1.1.0 2010-04-12 (修复一些bug)
    • 31 |
    • 1.1.0 2010-03-12 (twitter API支持)
    • 32 |
    • 1.0.0 2010-03-09 (核心功能实现)
    • 33 |
    34 |

    最后, 我希望能持续完善这个博客功能, 暂定名为pypress.

    35 |
    36 |
    37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /pypress/templates/blog/links.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% from "macros/_page.html" import paginate %} 4 | 5 | {%- block content %} 6 |
    7 |

    {{ _("Links") }}

    8 | 30 |
    31 | {%- endblock %} 32 | 33 | -------------------------------------------------------------------------------- /pypress/views/feeds.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | feeds.py 5 | ~~~~~~~~~~~~~ 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | import datetime 10 | import os 11 | 12 | from werkzeug.contrib.atom import AtomFeed 13 | 14 | from flask import Module, request, url_for 15 | 16 | from pypress.helpers import cached 17 | 18 | from pypress.models import User, Post, Tag 19 | 20 | feeds = Module(__name__) 21 | 22 | class PostFeed(AtomFeed): 23 | 24 | def add_post(self, post): 25 | 26 | self.add(post.title, 27 | unicode(post.content), 28 | content_type="html", 29 | author=post.author.username, 30 | url=post.permalink, 31 | updated=post.update_time, 32 | published=post.created_date) 33 | 34 | 35 | @feeds.route("/") 36 | @cached() 37 | def index(): 38 | feed = PostFeed("laoqiu blog - lastest", 39 | feed_url=request.url, 40 | url=request.url_root) 41 | 42 | posts = Post.query.order_by('created_date desc').limit(15) 43 | 44 | for post in posts: 45 | feed.add_post(post) 46 | 47 | return feed.get_response() 48 | 49 | 50 | @feeds.route("/tag//") 51 | @cached() 52 | def tag(slug): 53 | 54 | tag = Tag.query.filter_by(slug=slug).first_or_404() 55 | 56 | feed = PostFeed("laoqiu blog - %s" % tag, 57 | feed_url=request.url, 58 | url=request.url_root) 59 | 60 | posts = tag.posts.limit(15) 61 | 62 | for post in posts: 63 | feed.add_post(post) 64 | 65 | return feed.get_response() 66 | 67 | 68 | -------------------------------------------------------------------------------- /pypress/templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | {% from "macros/_forms.html" import render_errors %} 3 | 4 | {%- block content %} 5 |

    {{ _('Sign up to pypress') }}

    6 |
    7 |
    8 | {{ form.hidden_tag() }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
    {{ form.username.label }}{{ form.username(size=30) }} {{ render_errors(form.username) }}
    {{ form.nickname.label }}{{ form.nickname(size=30) }} {{ render_errors(form.nickname) }}
    {{ form.email.label }}{{ form.email(size=30) }} {{ render_errors(form.email) }}
    {{ form.password.label }}{{ form.password(size=30) }} {{ render_errors(form.password) }}
    {{ form.password_again.label }}{{ form.password_again(size=30) }} {{ render_errors(form.password_again) }}
    {{ form.code.label }}{{ form.code(size=15) }} {{ render_errors(form.code) }}
    {{ form.submit }}
    41 |
    42 |
    43 | {%- endblock %} 44 | {%- block sidebar %}{%- endblock %} 45 | -------------------------------------------------------------------------------- /pypress/templates/blog/add_link.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% from "macros/_forms.html" import render_errors %} 4 | 5 | {%- block content %} 6 |
    7 |

    {{ _("Add link") }}

    8 | 41 |
    42 | {%- endblock %} 43 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | import uuid 5 | 6 | from flask import Flask, current_app 7 | from flaskext.script import Server, Shell, Manager, Command, prompt_bool 8 | 9 | from pypress import create_app 10 | from pypress.extensions import db 11 | from pypress.models.users import User, UserCode 12 | 13 | manager = Manager(create_app('config.cfg')) 14 | 15 | manager.add_command("runserver", Server('0.0.0.0',port=8080)) 16 | 17 | def _make_context(): 18 | return dict(db=db) 19 | manager.add_command("shell", Shell(make_context=_make_context)) 20 | 21 | @manager.command 22 | def createall(): 23 | "Creates database tables" 24 | db.create_all() 25 | 26 | @manager.command 27 | def dropall(): 28 | "Drops all database tables" 29 | 30 | if prompt_bool("Are you sure ? You will lose all your data !"): 31 | db.drop_all() 32 | 33 | @manager.option('-r', '--role', dest='role', default="member") 34 | @manager.option('-n', '--number', dest='number', default=1, type=int) 35 | def createcode(role, number): 36 | codes = [] 37 | usercodes = [] 38 | for i in range(number): 39 | code = unicode(uuid.uuid4()).split('-')[0] 40 | codes.append(code) 41 | usercode = UserCode() 42 | usercode.code = code 43 | if role == "admin": 44 | usercode.role = User.ADMIN 45 | elif role == "moderator": 46 | usercode.role = User.MODERATOR 47 | else: 48 | usercode.role = User.MEMBER 49 | usercodes.append(usercode) 50 | if number==1: 51 | db.session.add(usercode) 52 | else: 53 | db.session.add_all(usercodes) 54 | db.session.commit() 55 | print "Sign up code:" 56 | for i in codes: 57 | print i 58 | return 59 | 60 | 61 | if __name__ == "__main__": 62 | manager.run() 63 | -------------------------------------------------------------------------------- /pypress/templates/blog/add_comment.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% from "macros/_page.html" import paginate %} 4 | {% from "macros/_forms.html" import render_errors %} 5 | 6 | {%- block content %} 7 |
    8 |

    {{ post.title }}

    9 | {%- if parent %} 10 |
    {{ parent.markdown }}
    11 | {%- endif %} 12 |
    13 |
    14 | {{ form.hidden_tag() }} 15 | 16 | 17 | {%- if g.user %} 18 | {{ form.email(type='hidden',value=g.user.email) }} 19 | {{ form.nickname(type='hidden',value=g.user.nickname) }} 20 | {%- else %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {%- endif %} 42 | 43 |
    {{ 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") }}
    44 |
    45 |
    46 |
    47 | {%- endblock %} 48 | -------------------------------------------------------------------------------- /pypress/templates/blog/list.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% from "macros/_page.html" import paginate %} 4 | 5 | {%- block content %} 6 |
    7 | {%- if page_obj.items %} 8 | {%- for post in page_obj.items %} 9 |
    10 |
    {{ post.author.nickname }}
    11 |

    {{ post.title }}

    12 | 15 | {% if post.tags %}{% endif %} 18 |
    19 | {{ post.summary|endtags|code_highlight|safe }} 20 |
    21 | 22 | {%- if post.update_date %} 23 | 26 | {%- endif %} 27 |
    28 | {%- endfor %} 29 | {{ paginate(page_obj, page_url) }} 30 | {%- else %} 31 |

    {{ _("Nobody's posted anything yet.") }}

    32 | {%- endif %} 33 |
    34 | {%- endblock %} 35 | {%- block sidebar %} 36 | 44 | {%- endblock %} 45 | -------------------------------------------------------------------------------- /pypress/static/js/jquery.idTabs.min.js: -------------------------------------------------------------------------------- 1 | /* idTabs ~ Sean Catchpole - Version 2.2 - MIT/GPL */ 2 | (function(){var dep={"jQuery":"http://code.jquery.com/jquery-latest.min.js"};var init=function(){(function($){$.fn.idTabs=function(){var s={};for(var i=0;i/") 25 | def index(page=1): 26 | 27 | links = Link.query 28 | 29 | if g.user is None: 30 | links = links.filter(Link.passed==True) 31 | 32 | page_obj = links.paginate(page=page, per_page=Link.PER_PAGE) 33 | 34 | page_url = lambda page: url_for("link.index",page=page) 35 | 36 | return render_template("blog/links.html", 37 | page_obj=page_obj, 38 | page_url=page_url) 39 | 40 | 41 | @link.route("/add/", methods=("GET","POST")) 42 | def add(): 43 | 44 | form = LinkForm() 45 | 46 | if form.validate_on_submit(): 47 | 48 | link = Link() 49 | form.populate_obj(link) 50 | 51 | if g.user and g.user.is_moderator: 52 | link.passed = True 53 | 54 | db.session.add(link) 55 | db.session.commit() 56 | 57 | flash(_("Adding success"), "success") 58 | 59 | return redirect(url_for('link.index')) 60 | 61 | return render_template("blog/add_link.html", form=form) 62 | 63 | 64 | @link.route("//pass/", methods=("POST",)) 65 | @auth.require(401) 66 | def edit(link_id): 67 | 68 | link = Link.query.get_or_404(link_id) 69 | link.permissions.edit.test(403) 70 | 71 | link.passed = True 72 | db.session.commit() 73 | 74 | return jsonify(success=True, 75 | link_id=link_id) 76 | 77 | 78 | @link.route("//delete/", methods=("POST",)) 79 | @auth.require(401) 80 | def delete(link_id): 81 | 82 | link = Link.query.get_or_404(link_id) 83 | link.permissions.delete.test(403) 84 | 85 | db.session.delete(link) 86 | db.session.commit() 87 | 88 | return jsonify(success=True, 89 | link_id=link_id) 90 | 91 | 92 | -------------------------------------------------------------------------------- /pypress/templates/blog/people.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% from "macros/_page.html" import paginate %} 4 | {% from "macros/_forms.html" import render_errors %} 5 | 6 | {%- block content %} 7 |
    8 |
    9 |
    {{ people.nickname }}
    10 |
    11 |

    {{ people.nickname }}

    12 |

    {{ _("Joined in") }}:{{ people.date_joined }}

    13 |
    14 |
    15 | 16 | {%- if people==g.user and config.get('TWITTER_KEY','') %} 17 |
    18 |
    19 | {{ form.hidden_tag() }} 20 |

    {{ form.content }}

    21 |

    {{ form.submit(class="button") }} {{ render_errors(form.content) }}

    22 |
    23 |
    24 | {%- endif %} 25 | 26 |
    27 | 31 |
    32 | 33 |
    34 | {%- if page_obj.items %} 35 | {%- for post in page_obj.items %} 36 |
    37 |

    {{ post.title }}

    38 | 42 |
    43 | {%- endfor %} 44 | {{ paginate(page_obj, page_url) }} 45 | {%- else %} 46 | {%- if g.user == people %} 47 |

    {{ _("You has not posted anything yet.") }} {{ _("Submit") }}

    48 | {%- else %} 49 |

    {{ _("%(name)s has not posted anything yet.", name=people.nickname) }}

    50 | {%- endif %} 51 | {%- endif %} 52 |
    53 |
    54 | {% from "macros/_twitter.html" import tweet_box %} 55 | {{ tweet_box(people.tweets) }} 56 |
    57 |
    58 | {%- endblock %} 59 | {%- block sidebar %} 60 | 67 | {%- endblock %} 68 | {%- block js %} 69 | {{ super() }} 70 | 71 | 76 | {%- endblock %} 77 | -------------------------------------------------------------------------------- /pypress/templates/macros/_post.html: -------------------------------------------------------------------------------- 1 | {% macro render_comment(comment) %} 2 |
  • 3 |
    4 | {{ comment.author.nickname }} 5 |
    6 |
    7 | {{ comment.author.nickname }} 8 | {{ comment.created_date|timesince }} 9 | | 10 |
    11 |
    12 | 13 | {{ comment.markdown }} 14 | 15 | {% if g.user %} 16 |
    17 | {% if comment.permissions.reply %} 18 | {{ _("reply") }} 19 | 29 | {% endif %} 30 | 31 | {% if comment.permissions.delete %} 32 | {{ _("delete") }} 33 | 38 | {% endif %} 39 |
    40 | 41 | {% endif %} 42 |
    43 | 44 | {% if comment.comments %} 45 |
    46 |
      47 | {% for child_comment in comment.comments %} 48 | {{ render_comment(child_comment) }} 49 | {% endfor %} 50 |
    51 |
    52 | {% endif %} 53 | 54 |
  • 55 | {% endmacro %} 56 | -------------------------------------------------------------------------------- /pypress/forms/blog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | """ 5 | forms: blog.py 6 | ~~~~~~~~~~~~~ 7 | 8 | :license: BSD, see LICENSE for more details. 9 | """ 10 | from flaskext.wtf import Form, TextAreaField, SubmitField, TextField, \ 11 | ValidationError, required, email, url, optional 12 | 13 | from flaskext.babel import gettext, lazy_gettext as _ 14 | 15 | from pypress.helpers import slugify 16 | from pypress.extensions import db 17 | from pypress.models import Post 18 | 19 | class PostForm(Form): 20 | 21 | title = TextField(_("Title"), validators=[ 22 | required(message=_("Title required"))]) 23 | 24 | slug = TextField(_("Slug")) 25 | 26 | content = TextAreaField(_("Content")) 27 | 28 | tags = TextField(_("Tags"), validators=[ 29 | required(message=_("Tags required"))]) 30 | 31 | submit = SubmitField(_("Save")) 32 | 33 | def __init__(self, *args, **kwargs): 34 | self.post = kwargs.get('obj', None) 35 | super(PostForm, self).__init__(*args, **kwargs) 36 | 37 | def validate_slug(self, field): 38 | if len(field.data) > 50: 39 | raise ValidationError, gettext("Slug must be less than 50 characters") 40 | slug = slugify(field.data) if field.data else slugify(self.title.data)[:50] 41 | posts = Post.query.filter_by(slug=slug) 42 | if self.post: 43 | posts = posts.filter(db.not_(Post.id==self.post.id)) 44 | if posts.count(): 45 | error = gettext("This slug is taken") if field.data else gettext("Slug is required") 46 | raise ValidationError, error 47 | 48 | 49 | class CommentForm(Form): 50 | 51 | email = TextField(_("Email"), validators=[ 52 | required(message=_("Email required")), 53 | email(message=_("A valid email address is required"))]) 54 | 55 | nickname = TextField(_("Nickname"), validators=[ 56 | required(message=_("Nickname required"))]) 57 | 58 | website = TextField(_("Website"), validators=[ 59 | optional(), 60 | url(message=_("A valid url is required"))]) 61 | 62 | comment = TextAreaField(_("Comment"), validators=[ 63 | required(message=_("Comment required"))]) 64 | 65 | submit = SubmitField(_("Add comment")) 66 | cancel = SubmitField(_("Cancel")) 67 | 68 | 69 | class LinkForm(Form): 70 | 71 | name = TextField(_("Site name"), validators=[ 72 | required(message=_("Name required"))]) 73 | 74 | link = TextField(_("link"), validators=[ 75 | url(message=_("A valid url is required"))]) 76 | 77 | email = TextField(_("Email"), validators=[ 78 | email(message=_("A valid email is required"))]) 79 | 80 | logo = TextField(_("Logo"), validators=[ 81 | optional(), 82 | url(message=_("A valid url is required"))]) 83 | 84 | description = TextAreaField(_("Description")) 85 | 86 | submit = SubmitField(_("Save")) 87 | 88 | 89 | class TemplateForm(Form): 90 | 91 | html = TextAreaField(_("HTML"), validators=[ 92 | required(message=_("HTML required"))]) 93 | 94 | submit = SubmitField(_("Save")) 95 | cancel = SubmitField(_("Cancel")) 96 | 97 | -------------------------------------------------------------------------------- /pypress/themes/default/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block title %}{% endblock %} Team blog 10 | {%- block css %} 11 | 12 | 13 | {%- endblock %} 14 | {%- block js %} 15 | 16 | 17 | 34 | {%- endblock %} 35 | 36 | 37 | 38 |
    39 | 54 |
    55 | {%- if get_flashed_messages() -%} 56 |
    57 | {% for category, msg in get_flashed_messages(with_categories=true) %} 58 | {{ msg }} 59 | {% endfor %} 60 |
    61 | {%- endif -%} 62 | {%- block content %}{%- endblock %} 63 | {%- block sidebar %} 64 | 71 | {%- endblock %} 72 |
    73 | 78 |
    79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /pypress/forms/account.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | """ 5 | forms: account.py 6 | ~~~~~~~~~~~~~ 7 | 8 | :license: BSD, see LICENSE for more details. 9 | """ 10 | from flaskext.wtf import Form, TextAreaField, HiddenField, BooleanField, \ 11 | PasswordField, SubmitField, TextField, ValidationError, \ 12 | required, email, equal_to, regexp 13 | 14 | from flaskext.babel import gettext, lazy_gettext as _ 15 | 16 | from pypress.extensions import db 17 | from pypress.models import User 18 | 19 | from .validators import is_username 20 | 21 | class LoginForm(Form): 22 | 23 | login = TextField(_("Username or email address"), validators=[ 24 | required(message=\ 25 | _("You must provide an email or username"))]) 26 | 27 | password = PasswordField(_("Password")) 28 | 29 | remember = BooleanField(_("Remember me")) 30 | 31 | next = HiddenField() 32 | 33 | submit = SubmitField(_("Login")) 34 | 35 | 36 | class SignupForm(Form): 37 | 38 | username = TextField(_("Username"), validators=[ 39 | required(message=_("Username required")), 40 | is_username]) 41 | 42 | nickname = TextField(_("Nickname"), validators=[ 43 | required(message=_("Nickname required"))]) 44 | 45 | password = PasswordField(_("Password"), validators=[ 46 | required(message=_("Password required"))]) 47 | 48 | password_again = PasswordField(_("Password again"), validators=[ 49 | equal_to("password", message=\ 50 | _("Passwords don't match"))]) 51 | 52 | email = TextField(_("Email address"), validators=[ 53 | required(message=_("Email address required")), 54 | email(message=_("A valid email address is required"))]) 55 | 56 | code = TextField(_("Signup Code")) 57 | 58 | next = HiddenField() 59 | 60 | submit = SubmitField(_("Signup")) 61 | 62 | def validate_username(self, field): 63 | user = User.query.filter(User.username.like(field.data)).first() 64 | if user: 65 | raise ValidationError, gettext("This username is taken") 66 | 67 | def validate_email(self, field): 68 | user = User.query.filter(User.email.like(field.data)).first() 69 | if user: 70 | raise ValidationError, gettext("This email is taken") 71 | 72 | 73 | class RecoverPasswordForm(Form): 74 | 75 | email = TextField("Your email address", validators=[ 76 | email(message=_("A valid email address is required"))]) 77 | 78 | submit = SubmitField(_("Find password")) 79 | 80 | 81 | class ChangePasswordForm(Form): 82 | 83 | password_old = PasswordField(_("Password"), validators=[ 84 | required(message=_("Password is required"))]) 85 | 86 | password = PasswordField(_("New Password"), validators=[ 87 | required(message=_("New Password is required"))]) 88 | 89 | password_again = PasswordField(_("Password again"), validators=[ 90 | equal_to("password", message=\ 91 | _("Passwords don't match"))]) 92 | 93 | submit = SubmitField(_("Save")) 94 | 95 | 96 | class DeleteAccountForm(Form): 97 | 98 | recaptcha = TextField(_("Recaptcha")) 99 | 100 | submit = SubmitField(_("Delete")) 101 | 102 | 103 | class TwitterForm(Form): 104 | 105 | content = TextAreaField(_("Content"), validators=[ 106 | required(message=_("Content is required"))]) 107 | 108 | submit = SubmitField(_("Send")) 109 | 110 | -------------------------------------------------------------------------------- /pypress/views/post.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | views: post.py 5 | ~~~~~~~~~~~~~ 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | import datetime 10 | import os 11 | import json 12 | 13 | from flask import Module, Response, request, flash, jsonify, g, current_app, \ 14 | abort, redirect, url_for, session 15 | 16 | from flaskext.mail import Message 17 | from flaskext.babel import gettext as _ 18 | 19 | from pypress import signals 20 | from pypress.helpers import render_template, cached, ip2long 21 | from pypress.permissions import auth 22 | from pypress.extensions import db 23 | 24 | from pypress.models import User, Post, Comment 25 | from pypress.forms import PostForm, CommentForm 26 | 27 | post = Module(__name__) 28 | 29 | 30 | @post.route("/", methods=("GET","POST")) 31 | @auth.require(401) 32 | def submit(): 33 | 34 | form = PostForm() 35 | 36 | if form.validate_on_submit(): 37 | 38 | post = Post(author=g.user) 39 | form.populate_obj(post) 40 | 41 | db.session.add(post) 42 | db.session.commit() 43 | 44 | flash(_("Posting success"), "success") 45 | 46 | return redirect(post.url) 47 | 48 | return render_template("blog/submit.html", form=form) 49 | 50 | 51 | @post.route("//", methods=("GET","POST")) 52 | def view(post_id): 53 | 54 | post = Post.query.get_or_404(post_id) 55 | 56 | return redirect(post.url) 57 | 58 | 59 | @post.route("//edit/", methods=("GET","POST")) 60 | @auth.require(401) 61 | def edit(post_id): 62 | 63 | post = Post.query.get_or_404(post_id) 64 | 65 | form = PostForm(title = post.title, 66 | slug = post.slug, 67 | content = post.content, 68 | tags = post.tags, 69 | obj = post) 70 | 71 | if form.validate_on_submit(): 72 | 73 | form.populate_obj(post) 74 | 75 | db.session.commit() 76 | 77 | flash(_("Post has been changed"), "success") 78 | 79 | return redirect(post.url) 80 | 81 | return render_template("blog/submit.html", form=form) 82 | 83 | 84 | @post.route("//delete/", methods=("GET","POST")) 85 | @auth.require(401) 86 | def delete(post_id): 87 | 88 | post = Post.query.get_or_404(post_id) 89 | post.permissions.delete.test(403) 90 | 91 | Comment.query.filter_by(post=post).delete() 92 | 93 | db.session.delete(post) 94 | db.session.commit() 95 | 96 | if g.user.id != post.author_id: 97 | body = render_template("emails/post_deleted.html", 98 | post=post) 99 | 100 | message = Message(subject="Your post has been deleted", 101 | body=body, 102 | recipients=[post.author.email]) 103 | 104 | mail.send(message) 105 | 106 | flash(_("The post has been deleted"), "success") 107 | 108 | return jsonify(success=True, 109 | redirect_url=url_for('frontend.index')) 110 | 111 | 112 | @post.route("//addcomment/", methods=("GET", "POST")) 113 | @post.route("///reply/", methods=("GET", "POST")) 114 | def add_comment(post_id, parent_id=None): 115 | 116 | post = Post.query.get_or_404(post_id) 117 | 118 | parent = Comment.query.get_or_404(parent_id) if parent_id else None 119 | 120 | form = CommentForm() 121 | 122 | if form.validate_on_submit(): 123 | 124 | comment = Comment(post=post, 125 | parent=parent, 126 | ip=ip2long(request.environ['REMOTE_ADDR'])) 127 | form.populate_obj(comment) 128 | 129 | if g.user: 130 | comment.author = g.user 131 | 132 | db.session.add(comment) 133 | db.session.commit() 134 | 135 | signals.comment_added.send(post) 136 | 137 | flash(_("Thanks for your comment"), "success") 138 | 139 | return redirect(comment.url) 140 | 141 | return render_template("blog/add_comment.html", 142 | parent=parent, 143 | post=post, 144 | form=form) 145 | 146 | 147 | -------------------------------------------------------------------------------- /pypress/themes/default/static/highlight.css: -------------------------------------------------------------------------------- 1 | div.highlight .hll { background-color: #ffffcc } 2 | div.highlight { border: 1px solid #c6c6c6; margin: 0px; padding: 2px 10px; overflow: auto; width: 550px; background: #f2f2f2; } 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/templates/blog/view.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% from "macros/_page.html" import paginate %} 4 | {% from "macros/_post.html" import render_comment with context %} 5 | 6 | {%- block content %} 7 |
    8 |
    9 |
    {{ post.author.nickname }}
    10 |

    {{ post.title }}

    11 | 14 | {% if post.tags %}{% endif %} 16 |
    17 | {{ post.content|code_highlight|gistcode|safe }} 18 |
    19 | 35 |
    36 | 37 | 41 | 42 | {%- if config.USE_LOCAL_COMMENT %} 43 |
    44 |
    45 |

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

    46 |
    47 | {{ comment_form.hidden_tag() }} 48 | {%- if g.user %} 49 | {{ comment_form.email(type='hidden',value=g.user.email) }} 50 | {{ comment_form.nickname(type='hidden',value=g.user.nickname) }} 51 | {{ comment_form.website(type='hidden') }} 52 | {%- else %} 53 |

    {{ comment_form.email(class="text") }} {{ comment_form.email.label }}

    54 |

    {{ comment_form.nickname(class="text") }} {{ comment_form.nickname.label }}

    55 |

    {{ comment_form.website(class="text") }} {{ comment_form.website.label }}

    56 | {%- endif %} 57 |

    {{ comment_form.comment(class="text") }}

    58 |

    {{ comment_form.submit(class="button") }}

    59 |
    60 |
    61 | 62 |
    63 |

    {{ _('Comments') }}

    64 | {%- if post.comments %} 65 |
      66 | {%- for comment in post.comments %} 67 | {{ render_comment(comment) }} 68 | {%- endfor %} 69 |
    70 | {%- else %} 71 |

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

    72 | {%- endif %} 73 |
    74 | 75 |
    76 | {%- else %} 77 | {% include 'blog/comment.html' %} 78 | {%- endif %} 79 |
    80 | {%- endblock %} 81 | 82 | {%- block sidebar %} 83 | 91 | {%- endblock %} 92 | 93 | {%- block js %} 94 | {{ super() }} 95 | 104 | {%- endblock %} 105 | -------------------------------------------------------------------------------- /pypress/templates/blog/submit.html: -------------------------------------------------------------------------------- 1 | {% extends theme("layout.html") %} 2 | 3 | {% from "macros/_forms.html" import render_errors %} 4 | 5 | {%- block content %} 6 |
    7 |

    {{ _('Submit a post') }}

    8 |
    9 | {{ form.hidden_tag() }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
    {{ form.title.label(style="display:block;width:100px;") }}{{ form.title(size=50, class="text") }} {{ render_errors(form.title) }}
    {{ form.slug.label }}{{ form.slug(size=50, class="text") }} {{ render_errors(form.slug) }}
    {{ form.content.label }}{{ form.content(style="width:820px;height:400px;") }} {{ render_errors(form.content) }}
    {{ form.tags.label }}{{ form.tags(size=50, class="text") }} {{ render_errors(form.tags) }}
    {{ form.submit(class="button") }}
    34 |
    35 | 36 |
    37 | {%- endblock %} 38 | {%- block css %} 39 | {{ super() }} 40 | 43 | 44 | 45 | 46 | {%- endblock %} 47 | {%- block js %} 48 | {{ super() }} 49 | 50 | 122 | {%- endblock %} 123 | {%- block sidebar %}{%- endblock %} 124 | -------------------------------------------------------------------------------- /pypress/views/account.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | account.py 5 | ~~~~~~~~~~~~~ 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | import datetime 10 | import os, sys 11 | 12 | # parse_qsl moved to urlparse module in v2.6 13 | try: 14 | from urlparse import parse_qsl 15 | except: 16 | from cgi import parse_qsl 17 | 18 | import oauth2 as oauth 19 | 20 | from flask import Module, Response, request, flash, jsonify, g, current_app,\ 21 | abort, redirect, url_for, session 22 | 23 | from flaskext.babel import gettext as _ 24 | from flaskext.principal import identity_changed, Identity, AnonymousIdentity 25 | 26 | from pypress.helpers import render_template, cached 27 | from pypress.permissions import auth, admin 28 | from pypress.extensions import db 29 | 30 | from pypress.models import User, UserCode, Twitter 31 | from pypress.forms import LoginForm, SignupForm 32 | 33 | from pypress import twitter 34 | 35 | account = Module(__name__) 36 | 37 | REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' 38 | ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' 39 | AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' 40 | SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' 41 | 42 | @account.route("/login/", methods=("GET","POST")) 43 | def login(): 44 | 45 | form = LoginForm(login=request.args.get('login',None), 46 | next=request.args.get('next',None)) 47 | 48 | if form.validate_on_submit(): 49 | 50 | user, authenticated = User.query.authenticate(form.login.data, 51 | form.password.data) 52 | 53 | if user and authenticated: 54 | session.permanent = form.remember.data 55 | 56 | identity_changed.send(current_app._get_current_object(), 57 | identity=Identity(user.id)) 58 | 59 | flash(_("Welcome back, %(name)s", name=user.username), "success") 60 | 61 | next_url = form.next.data 62 | 63 | if not next_url or next_url == request.path: 64 | next_url = url_for('frontend.people', username=user.username) 65 | 66 | return redirect(next_url) 67 | 68 | else: 69 | 70 | flash(_("Sorry, invalid login"), "error") 71 | 72 | return render_template("account/login.html", form=form) 73 | 74 | 75 | @account.route("/signup/", methods=("GET","POST")) 76 | def signup(): 77 | 78 | form = SignupForm(next=request.args.get('next',None)) 79 | 80 | if form.validate_on_submit(): 81 | 82 | code = UserCode.query.filter_by(code=form.code.data).first() 83 | 84 | if code: 85 | user = User(role=code.role) 86 | form.populate_obj(user) 87 | 88 | db.session.add(user) 89 | db.session.delete(code) 90 | db.session.commit() 91 | 92 | identity_changed.send(current_app._get_current_object(), 93 | identity=Identity(user.id)) 94 | 95 | flash(_("Welcome, %(name)s", name=user.nickname), "success") 96 | 97 | next_url = form.next.data 98 | 99 | if not next_url or next_url == request.path: 100 | next_url = url_for('frontend.people', username=user.username) 101 | 102 | return redirect(next_url) 103 | else: 104 | form.code.errors.append(_("Code is not allowed")) 105 | 106 | return render_template("account/signup.html", form=form) 107 | 108 | 109 | @account.route("/logout/") 110 | def logout(): 111 | 112 | flash(_("You are now logged out"), "success") 113 | identity_changed.send(current_app._get_current_object(), 114 | identity=AnonymousIdentity()) 115 | 116 | next_url = request.args.get('next','') 117 | 118 | if not next_url or next_url == request.path: 119 | next_url = url_for("frontend.index") 120 | 121 | return redirect(next_url) 122 | 123 | 124 | @account.route("/twitter/") 125 | @auth.require(401) 126 | def twitter(): 127 | 128 | if g.user.twitter: 129 | flash(_("You twitter's access token is already exists"), "error") 130 | return redirect(url_for('frontend.people', username=g.user.username)) 131 | 132 | consumer_key = current_app.config['TWITTER_KEY'] 133 | consumer_secret = current_app.config['TWITTER_SECRET'] 134 | 135 | signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() 136 | oauth_consumer = oauth.Consumer(key=consumer_key, secret=consumer_secret) 137 | oauth_client = oauth.Client(oauth_consumer) 138 | 139 | try: 140 | resp, content = oauth_client.request(REQUEST_TOKEN_URL, 'GET') 141 | except AttributeError: 142 | flash(_("Can not connect twitter.com")) 143 | return redirect(url_for('frontend.people',username=g.user.username)) 144 | 145 | if resp['status'] != '200': 146 | return 'Invalid respond from Twitter requesting temp token: %s' % resp['status'] 147 | else: 148 | request_token = dict(parse_qsl(content)) 149 | 150 | session['token'] = request_token 151 | 152 | return redirect('%s?oauth_token=%s' % (AUTHORIZATION_URL.replace("https:","http:"), 153 | request_token['oauth_token'])) 154 | 155 | 156 | @account.route("/twitter/callback") 157 | @auth.require(401) 158 | def twitter_callback(): 159 | token = oauth.Token(session['token']['oauth_token'], session['token']['oauth_token_secret']) 160 | verifier = request.args.get('oauth_verifier', '') 161 | token.set_verifier(verifier) 162 | 163 | consumer_key = current_app.config['TWITTER_KEY'] 164 | consumer_secret = current_app.config['TWITTER_SECRET'] 165 | oauth_consumer = oauth.Consumer(key=consumer_key, secret=consumer_secret) 166 | 167 | oauth_client = oauth.Client(oauth_consumer, token) 168 | resp, content = oauth_client.request(ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % verifier) 169 | access_token = dict(parse_qsl(content)) 170 | 171 | if resp['status'] != '200': 172 | return 'The request for a Token did not succeed: %s' % resp['status'] 173 | else: 174 | if g.user.twitter is None: 175 | g.user.twitter = Twitter() 176 | 177 | g.user.twitter.token = access_token['oauth_token'] 178 | g.user.twitter.token_secret = access_token['oauth_token_secret'] 179 | 180 | db.session.commit() 181 | 182 | flash(_("Twitter request success"), "success") 183 | 184 | return redirect(url_for('frontend.people', username=g.user.username)) 185 | 186 | 187 | -------------------------------------------------------------------------------- /pypress/models/users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | models: users.py 5 | ~~~~~~~~~~~~~ 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | import hashlib 10 | 11 | from datetime import datetime 12 | 13 | from werkzeug import cached_property 14 | 15 | from flask import abort, current_app 16 | 17 | from flaskext.sqlalchemy import BaseQuery 18 | from flaskext.principal import RoleNeed, UserNeed, Permission 19 | 20 | from pypress.extensions import db, cache 21 | from pypress.permissions import admin 22 | 23 | from pypress import twitter 24 | 25 | class UserQuery(BaseQuery): 26 | 27 | def from_identity(self, identity): 28 | """ 29 | Loads user from flaskext.principal.Identity instance and 30 | assigns permissions from user. 31 | 32 | A "user" instance is monkeypatched to the identity instance. 33 | 34 | If no user found then None is returned. 35 | """ 36 | 37 | try: 38 | user = self.get(int(identity.name)) 39 | except ValueError: 40 | user = None 41 | 42 | if user: 43 | identity.provides.update(user.provides) 44 | 45 | identity.user = user 46 | 47 | return user 48 | 49 | def authenticate(self, login, password): 50 | 51 | user = self.filter(db.or_(User.username==login, 52 | User.email==login)).first() 53 | 54 | if user: 55 | authenticated = user.check_password(password) 56 | else: 57 | authenticated = False 58 | 59 | return user, authenticated 60 | 61 | def search(self, key): 62 | query = self.filter(db.or_(User.email==key, 63 | User.nickname.ilike('%'+key+'%'), 64 | User.username.ilike('%'+key+'%'))) 65 | return query 66 | 67 | def get_by_username(self, username): 68 | user = self.filter(User.username==username).first() 69 | if user is None: 70 | abort(404) 71 | return user 72 | 73 | 74 | class User(db.Model): 75 | 76 | __tablename__ = 'users' 77 | 78 | query_class = UserQuery 79 | 80 | PER_PAGE = 50 81 | TWEET_PER_PAGE = 30 82 | 83 | MEMBER = 100 84 | MODERATOR = 200 85 | ADMIN = 300 86 | 87 | id = db.Column(db.Integer, primary_key=True) 88 | username = db.Column(db.String(20), unique=True) 89 | nickname = db.Column(db.String(20)) 90 | email = db.Column(db.String(100), unique=True, nullable=False) 91 | _password = db.Column("password", db.String(80), nullable=False) 92 | role = db.Column(db.Integer, default=MEMBER) 93 | activation_key = db.Column(db.String(40)) 94 | date_joined = db.Column(db.DateTime, default=datetime.utcnow) 95 | last_login = db.Column(db.DateTime, default=datetime.utcnow) 96 | last_request = db.Column(db.DateTime, default=datetime.utcnow) 97 | block = db.Column(db.Boolean, default=False) 98 | 99 | class Permissions(object): 100 | 101 | def __init__(self, obj): 102 | self.obj = obj 103 | 104 | @cached_property 105 | def edit(self): 106 | return Permission(UserNeed(self.obj.id)) & admin 107 | 108 | def __init__(self, *args, **kwargs): 109 | super(User, self).__init__(*args, **kwargs) 110 | 111 | def __str__(self): 112 | return self.nickname 113 | 114 | def __repr__(self): 115 | return "<%s>" % self 116 | 117 | @cached_property 118 | def permissions(self): 119 | return self.Permissions(self) 120 | 121 | def _get_password(self): 122 | return self._password 123 | 124 | def _set_password(self, password): 125 | self._password = hashlib.md5(password).hexdigest() 126 | 127 | password = db.synonym("_password", 128 | descriptor=property(_get_password, 129 | _set_password)) 130 | 131 | def check_password(self,password): 132 | if self.password is None: 133 | return False 134 | return self.password == hashlib.md5(password).hexdigest() 135 | 136 | @cached_property 137 | def provides(self): 138 | needs = [RoleNeed('authenticated'), 139 | UserNeed(self.id)] 140 | 141 | if self.is_moderator: 142 | needs.append(RoleNeed('moderator')) 143 | 144 | if self.is_admin: 145 | needs.append(RoleNeed('admin')) 146 | 147 | return needs 148 | 149 | @property 150 | def is_moderator(self): 151 | return self.role >= self.MODERATOR 152 | 153 | @property 154 | def is_admin(self): 155 | return self.role >= self.ADMIN 156 | 157 | @cached_property 158 | def twitter_api(self): 159 | if self.twitter and self.twitter.token \ 160 | and self.twitter.token_secret: 161 | api = twitter.Api(current_app.config['TWITTER_KEY'], 162 | current_app.config['TWITTER_SECRET'], 163 | self.twitter.token, 164 | self.twitter.token_secret) 165 | else: 166 | api = None 167 | 168 | return api 169 | 170 | @cached_property 171 | def tweets(self): 172 | api = self.twitter_api 173 | if api: 174 | info = api.VerifyCredentials() 175 | try: 176 | tweets = api.GetUserTimeline(screen_name=info.screen_name, count=self.TWEET_PER_PAGE) 177 | except: 178 | return [] 179 | else: 180 | return [] 181 | return tweets 182 | 183 | def post_twitter(self, content): 184 | 185 | api = self.twitter_api 186 | if api: 187 | status = api.PostUpdate(content) 188 | else: 189 | return False 190 | 191 | return True 192 | 193 | 194 | class UserCode(db.Model): 195 | 196 | __tablename__ = 'usercode' 197 | 198 | id = db.Column(db.Integer, primary_key=True) 199 | code = db.Column(db.String(20), nullable=False) 200 | role = db.Column(db.Integer, default=User.MEMBER) 201 | 202 | def __init__(self, *args, **kwargs): 203 | super(UserCode, self).__init__(*args, **kwargs) 204 | 205 | def __str__(self): 206 | return self.code 207 | 208 | def __repr__(self): 209 | return "<%s>" % self 210 | 211 | 212 | class Twitter(db.Model): 213 | 214 | __tablename__ = 'twitter' 215 | 216 | id = db.Column(db.Integer, primary_key=True) 217 | 218 | user_id = db.Column(db.Integer, 219 | db.ForeignKey(User.id, ondelete='CASCADE'), 220 | nullable=False, 221 | unique=True) 222 | 223 | token = db.Column(db.String(50)) 224 | token_secret = db.Column(db.String(50)) 225 | 226 | def __init__(self, *args, **kwargs): 227 | super(Twitter, self).__init__(*args, **kwargs) 228 | 229 | def __str__(self): 230 | return self.user_id 231 | 232 | def __repr__(self): 233 | return "<%s>" % self 234 | 235 | 236 | User.twitter = db.relation(Twitter, backref="user", uselist=False) 237 | 238 | -------------------------------------------------------------------------------- /pypress/views/frontend.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | frontend.py 5 | ~~~~~~~~~~~~~ 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | import datetime 10 | import os 11 | 12 | from flask import Module, Response, request, flash, jsonify, g, current_app,\ 13 | abort, redirect, url_for, session, send_file, send_from_directory 14 | 15 | from flaskext.babel import gettext as _ 16 | 17 | from pypress.helpers import render_template, cached 18 | from pypress.permissions import auth, admin 19 | from pypress.extensions import db, photos 20 | 21 | from pypress.models import User, Post, Comment, Tag 22 | from pypress.forms import CommentForm, TemplateForm, TwitterForm 23 | 24 | frontend = Module(__name__) 25 | 26 | @frontend.route("/") 27 | @frontend.route("/page//") 28 | @frontend.route("//") 29 | @frontend.route("//page//") 30 | @frontend.route("///") 31 | @frontend.route("///page//") 32 | @frontend.route("////") 33 | @frontend.route("////page//") 34 | def index(year=None, month=None, day=None, page=1): 35 | 36 | if page<1:page=1 37 | 38 | page_obj = Post.query.archive(year,month,day).as_list() \ 39 | .paginate(page, per_page=Post.PER_PAGE) 40 | 41 | page_url = lambda page: url_for("post.index", 42 | year=year, 43 | month=month, 44 | day=day, 45 | page=page) 46 | 47 | return render_template("blog/list.html", 48 | page_obj=page_obj, 49 | page_url=page_url) 50 | 51 | 52 | @frontend.route("/search/") 53 | @frontend.route("/search/page//") 54 | def search(page=1): 55 | 56 | keywords = request.args.get('q','').strip() 57 | 58 | if not keywords: 59 | return redirect(url_for("frontend.index")) 60 | 61 | page_obj = Post.query.search(keywords).as_list() \ 62 | .paginate(page, per_page=Post.PER_PAGE) 63 | 64 | if page_obj.total == 1: 65 | 66 | post = page_obj.items[0] 67 | return redirect(post.url) 68 | 69 | page_url = lambda page: url_for('frontend.search', 70 | page=page, 71 | keywords=keywords) 72 | 73 | return render_template("blog/search_result.html", 74 | page_obj=page_obj, 75 | page_url=page_url, 76 | keywords=keywords) 77 | 78 | 79 | @frontend.route("/archive/") 80 | def archive(): 81 | 82 | 83 | return render_template("blog/archive.html") 84 | 85 | 86 | @frontend.route("/tags/") 87 | def tags(): 88 | 89 | return render_template("blog/tags.html") 90 | 91 | 92 | @frontend.route("/tags//") 93 | @frontend.route("/tags//page//") 94 | def tag(slug, page=1): 95 | 96 | tag = Tag.query.filter_by(slug=slug).first_or_404() 97 | 98 | page_obj = tag.posts.as_list() \ 99 | .paginate(page, per_page=Post.PER_PAGE) 100 | 101 | page_url = lambda page: url_for("post.tag", 102 | slug=slug, 103 | page=page) 104 | 105 | return render_template("blog/list.html", 106 | page_obj=page_obj, 107 | page_url=page_url) 108 | 109 | 110 | @frontend.route("/people//", methods=("GET","POST")) 111 | @frontend.route("/people//page//", methods=("GET","POST")) 112 | def people(username, page=1): 113 | 114 | people = User.query.get_by_username(username) 115 | 116 | form = TwitterForm() 117 | 118 | if form.validate_on_submit(): 119 | 120 | api = people.twitter_api 121 | 122 | if api is None: 123 | return redirect(url_for('account.twitter')) 124 | 125 | content = form.content.data.encode("utf8") 126 | 127 | status = people.post_twitter(content) 128 | 129 | if status: 130 | flash(_("Twitter posting is success"), "success") 131 | 132 | return redirect(url_for('frontend.people', 133 | username=username, 134 | page=page)) 135 | else: 136 | flash(_("Twitter posting is failed"), "error") 137 | 138 | page_obj = Post.query.filter(Post.author_id==people.id).as_list() \ 139 | .paginate(page, per_page=Post.PER_PAGE) 140 | 141 | page_url = lambda page: url_for("post.people", 142 | username=username, 143 | page=page) 144 | 145 | return render_template("blog/people.html", 146 | form=form, 147 | page_obj=page_obj, 148 | page_url=page_url, 149 | people=people) 150 | 151 | 152 | @frontend.route("/upload/", methods=("POST",)) 153 | @auth.require(401) 154 | def upload(): 155 | 156 | if 'Filedata' in request.files: 157 | filename = photos.save(request.files['Filedata']) 158 | return json.dumps({'imgUrl':photos.url(filename)}) 159 | 160 | return json.dumps({'error':_("Please select a picture")}) 161 | 162 | 163 | @frontend.route("/about/") 164 | def about(): 165 | return render_template("blog/about.html") 166 | 167 | 168 | @frontend.route("/////") 169 | def post(year, month, day, slug): 170 | 171 | post = Post.query.get_by_slug(slug) 172 | 173 | date = (post.created_date.year, 174 | post.created_date.month, 175 | post.created_date.day) 176 | 177 | if date != (year, month, day): 178 | return redirect(post.url) 179 | 180 | prev_post = Post.query.filter(Post.created_datepost.created_date) \ 183 | .order_by('created_date asc').first() 184 | 185 | return render_template("blog/view.html", 186 | post=post, 187 | prev_post=prev_post, 188 | next_post=next_post, 189 | comment_form=CommentForm()) 190 | 191 | 192 | @frontend.route("//") 193 | @frontend.route("///") 194 | def _post(slug, date=None): 195 | 196 | post = Post.query.get_by_slug(slug) 197 | 198 | return redirect(post.url) 199 | 200 | 201 | @frontend.route("/template//", methods=("GET","POST")) 202 | @admin.require(401) 203 | def template_edit(path): 204 | 205 | path = os.path.join(current_app.root_path, 'templates', "%s.html" % path) 206 | html = "" 207 | 208 | try: 209 | f = open(path) 210 | html = f.read() 211 | f.close() 212 | except: 213 | flash(_("Template file does not exists"), "error") 214 | 215 | form = TemplateForm(html=html.decode('utf8')) 216 | 217 | if form.validate_on_submit(): 218 | 219 | f = open(path, 'w') 220 | f.write(form.html.data.encode('utf8')) 221 | f.close() 222 | 223 | flash(_("Saving success"), "success") 224 | 225 | return redirect(url_for("frontend.index")) 226 | 227 | return render_template("blog/template_edit.html", 228 | form=form, 229 | path=path) 230 | 231 | 232 | @frontend.route("/favicon.ico") 233 | def favicon(): 234 | return send_from_directory(os.path.join(current_app.root_path, 'static'), 235 | 'favicon.ico', mimetype='image/vnd.microsoft.icon') 236 | 237 | 238 | -------------------------------------------------------------------------------- /pypress/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | """ 5 | __init__.py 6 | ~~~~~~~~~~~~~ 7 | 8 | :license: BSD, see LICENSE for more details. 9 | """ 10 | import os 11 | import logging 12 | import datetime 13 | 14 | from logging.handlers import SMTPHandler, RotatingFileHandler 15 | from werkzeug import parse_date 16 | 17 | from flask import Flask, g, session, request, flash, redirect, jsonify, url_for 18 | 19 | from flaskext.babel import Babel, gettext as _ 20 | from flaskext.themes import setup_themes 21 | from flaskext.principal import Principal, RoleNeed, UserNeed, identity_loaded 22 | from flaskext.uploads import configure_uploads 23 | 24 | from pypress import views, helpers 25 | from pypress.models import User, Post, Tag, Link, Comment 26 | from pypress.extensions import db, mail, cache, photos 27 | from pypress.helpers import render_template 28 | 29 | DEFAULT_APP_NAME = 'pypress' 30 | 31 | DEFAULT_MODULES = ( 32 | (views.frontend, ""), 33 | (views.post, "/post"), 34 | (views.comment, "/comment"), 35 | (views.account, "/account"), 36 | (views.link, "/link"), 37 | (views.feeds, "/feeds"), 38 | ) 39 | 40 | def create_app(config=None, modules=None): 41 | 42 | if modules is None: 43 | modules = DEFAULT_MODULES 44 | 45 | app = Flask(DEFAULT_APP_NAME) 46 | 47 | # config 48 | app.config.from_pyfile(config) 49 | 50 | configure_extensions(app) 51 | 52 | configure_identity(app) 53 | configure_logging(app) 54 | configure_errorhandlers(app) 55 | configure_before_handlers(app) 56 | configure_template_filters(app) 57 | configure_context_processors(app) 58 | configure_uploads(app, (photos,)) 59 | 60 | configure_i18n(app) 61 | 62 | # register module 63 | configure_modules(app, modules) 64 | 65 | return app 66 | 67 | 68 | def configure_extensions(app): 69 | # configure extensions 70 | db.init_app(app) 71 | mail.init_app(app) 72 | cache.init_app(app) 73 | setup_themes(app) 74 | 75 | 76 | def configure_identity(app): 77 | 78 | principal = Principal(app) 79 | 80 | @identity_loaded.connect_via(app) 81 | def on_identity_loaded(sender, identity): 82 | g.user = User.query.from_identity(identity) 83 | 84 | 85 | def configure_i18n(app): 86 | 87 | babel = Babel(app) 88 | 89 | @babel.localeselector 90 | def get_locale(): 91 | accept_languages = app.config.get('ACCEPT_LANGUAGES',['en','zh']) 92 | return request.accept_languages.best_match(accept_languages) 93 | 94 | 95 | def configure_context_processors(app): 96 | 97 | @app.context_processor 98 | def tags(): 99 | tags = cache.get("tags") 100 | if tags is None: 101 | tags = Tag.query.cloud() 102 | cache.set("tags", tags) 103 | return dict(tags=tags) 104 | 105 | @app.context_processor 106 | def links(): 107 | links = cache.get("links") 108 | if links is None: 109 | links = Link.query.filter(Link.passed==True).limit(10).all() 110 | cache.set("links", links) 111 | return dict(links=links) 112 | 113 | @app.context_processor 114 | def archives(): 115 | archives = cache.get("archives") 116 | if archives is None: 117 | begin_post = Post.query.order_by('created_date').first() 118 | 119 | now = datetime.datetime.now() 120 | 121 | begin = begin_post.created_date if begin_post else now 122 | end = now 123 | 124 | total = (end.year-begin.year)*12 - begin.month + end.month 125 | archives = [begin] 126 | 127 | date = begin 128 | for i in range(total): 129 | if date.month<12: 130 | date = datetime.datetime(date.year,date.month+1,1) 131 | else: 132 | date = datetime.datetime(date.year+1, 1, 1) 133 | archives.append(date) 134 | archives.reverse() 135 | cache.set("archives", archives) 136 | 137 | return dict(archives=archives) 138 | 139 | @app.context_processor 140 | def latest_comments(): 141 | latest_comments = cache.get("latest_comments") 142 | if latest_comments is None: 143 | latest_comments = Comment.query.order_by(Comment.created_date.desc()) \ 144 | .limit(5).all() 145 | cache.set("latest_comments", latest_comments) 146 | return dict(latest_comments=latest_comments) 147 | 148 | @app.context_processor 149 | def config(): 150 | return dict(config=app.config) 151 | 152 | 153 | def configure_template_filters(app): 154 | 155 | @app.template_filter() 156 | def timesince(value): 157 | return helpers.timesince(value) 158 | 159 | @app.template_filter() 160 | def endtags(value): 161 | return helpers.endtags(value) 162 | 163 | @app.template_filter() 164 | def gravatar(email,size): 165 | return helpers.gravatar(email,size) 166 | 167 | @app.template_filter() 168 | def format_date(date,s='full'): 169 | return helpers.format_date(date,s) 170 | 171 | @app.template_filter() 172 | def format_datetime(time,s='full'): 173 | return helpers.format_datetime(time,s) 174 | 175 | @app.template_filter() 176 | def twitter_date(date): 177 | return parse_date(date) 178 | 179 | @app.template_filter() 180 | def code_highlight(html): 181 | return helpers.code_highlight(html) 182 | 183 | @app.template_filter() 184 | def gistcode(html): 185 | return helpers.gistcode(html) 186 | 187 | 188 | def configure_before_handlers(app): 189 | 190 | @app.before_request 191 | def authenticate(): 192 | g.user = getattr(g.identity, 'user', None) 193 | 194 | 195 | def configure_errorhandlers(app): 196 | 197 | @app.errorhandler(401) 198 | def unauthorized(error): 199 | if request.is_xhr: 200 | return jsonfiy(error=_("Login required")) 201 | flash(_("Please login to see this page"), "error") 202 | return redirect(url_for("account.login", next=request.path)) 203 | 204 | @app.errorhandler(403) 205 | def forbidden(error): 206 | if request.is_xhr: 207 | return jsonify(error=_('Sorry, page not allowed')) 208 | return render_template("errors/403.html", error=error) 209 | 210 | @app.errorhandler(404) 211 | def page_not_found(error): 212 | if request.is_xhr: 213 | return jsonify(error=_('Sorry, page not found')) 214 | return render_template("errors/404.html", error=error) 215 | 216 | @app.errorhandler(500) 217 | def server_error(error): 218 | if request.is_xhr: 219 | return jsonify(error=_('Sorry, an error has occurred')) 220 | return render_template("errors/500.html", error=error) 221 | 222 | 223 | def configure_modules(app, modules): 224 | 225 | for module, url_prefix in modules: 226 | app.register_module(module, url_prefix=url_prefix) 227 | 228 | 229 | def configure_logging(app): 230 | 231 | mail_handler = \ 232 | SMTPHandler(app.config['MAIL_SERVER'], 233 | app.config['DEFAULT_MAIL_SENDER'], 234 | app.config['ADMINS'], 235 | 'application error', 236 | ( 237 | app.config['MAIL_USERNAME'], 238 | app.config['MAIL_PASSWORD'], 239 | )) 240 | 241 | mail_handler.setLevel(logging.ERROR) 242 | app.logger.addHandler(mail_handler) 243 | 244 | formatter = logging.Formatter( 245 | '%(asctime)s %(levelname)s: %(message)s ' 246 | '[in %(pathname)s:%(lineno)d]') 247 | 248 | debug_log = os.path.join(app.root_path, 249 | app.config['DEBUG_LOG']) 250 | 251 | debug_file_handler = \ 252 | RotatingFileHandler(debug_log, 253 | maxBytes=100000, 254 | backupCount=10) 255 | 256 | debug_file_handler.setLevel(logging.DEBUG) 257 | debug_file_handler.setFormatter(formatter) 258 | app.logger.addHandler(debug_file_handler) 259 | 260 | error_log = os.path.join(app.root_path, 261 | app.config['ERROR_LOG']) 262 | 263 | error_file_handler = \ 264 | RotatingFileHandler(error_log, 265 | maxBytes=100000, 266 | backupCount=10) 267 | 268 | error_file_handler.setLevel(logging.ERROR) 269 | error_file_handler.setFormatter(formatter) 270 | app.logger.addHandler(error_file_handler) 271 | 272 | -------------------------------------------------------------------------------- /pypress/helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | helpers.py 5 | ~~~~~~~~~~~~~ 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | import re 10 | import markdown 11 | import urlparse 12 | import functools 13 | import hashlib 14 | import socket, struct 15 | 16 | from datetime import datetime 17 | 18 | from pygments import highlight 19 | from pygments.lexers import get_lexer_by_name 20 | from pygments.formatters import HtmlFormatter 21 | 22 | from flask import current_app, g 23 | from flaskext.babel import gettext, ngettext, format_date, format_datetime 24 | from flaskext.themes import render_theme_template 25 | 26 | from pypress.extensions import cache 27 | 28 | class Storage(dict): 29 | """ 30 | A Storage object is like a dictionary except `obj.foo` can be used 31 | in addition to `obj['foo']`. 32 | >>> o = storage(a=1) 33 | >>> o.a 34 | 1 35 | >>> o['a'] 36 | 1 37 | >>> o.a = 2 38 | >>> o['a'] 39 | 2 40 | >>> del o.a 41 | >>> o.a 42 | Traceback (most recent call last): 43 | ... 44 | AttributeError: 'a' 45 | """ 46 | def __getattr__(self, key): 47 | try: 48 | return self[key] 49 | except KeyError, k: 50 | raise AttributeError, k 51 | 52 | def __setattr__(self, key, value): 53 | self[key] = value 54 | 55 | def __delattr__(self, key): 56 | try: 57 | del self[key] 58 | except KeyError, k: 59 | raise AttributeError, k 60 | 61 | def __repr__(self): 62 | return '' 63 | 64 | storage = Storage 65 | 66 | _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+') 67 | _pre_re = re.compile(r'
    (?P[\w\W]+?)
    ') 68 | _lang_re = re.compile(r'l=[\'"]?(?P\w+)[\'"]?') 69 | 70 | def slugify(text, delim=u'-'): 71 | """Generates an ASCII-only slug. From http://flask.pocoo.org/snippets/5/""" 72 | result = [] 73 | for word in _punct_re.split(text.lower()): 74 | #word = word.encode('translit/long') 75 | if word: 76 | result.append(word) 77 | return unicode(delim.join(result)) 78 | 79 | markdown = functools.partial(markdown.markdown, 80 | safe_mode='remove', 81 | output_format="html") 82 | 83 | cached = functools.partial(cache.cached, 84 | unless= lambda: g.user is not None) 85 | 86 | def get_theme(): 87 | return current_app.config['THEME'] 88 | 89 | def render_template(template, **context): 90 | return render_theme_template(get_theme(), template, **context) 91 | 92 | def request_wants_json(request): 93 | """ 94 | we only accept json if the quality of json is greater than the 95 | quality of text/html because text/html is preferred to support 96 | browsers that accept on */* 97 | """ 98 | best = request.accept_mimetypes \ 99 | .best_match(['application/json', 'text/html']) 100 | return best == 'application/json' and \ 101 | request.accept_mimetypes[best] > request.accept_mimetypes['text/html'] 102 | 103 | def timesince(dt, default=None): 104 | """ 105 | Returns string representing "time since" e.g. 106 | 3 days ago, 5 hours ago etc. 107 | """ 108 | 109 | if default is None: 110 | default = gettext("just now") 111 | 112 | now = datetime.utcnow() 113 | diff = now - dt 114 | 115 | years = diff.days / 365 116 | months = diff.days / 30 117 | weeks = diff.days / 7 118 | days = diff.days 119 | hours = diff.seconds / 3600 120 | minutes = diff.seconds / 60 121 | seconds = diff.seconds 122 | 123 | periods = ( 124 | (years, ngettext("%(num)s year", "%(num)s years", num=years)), 125 | (months, ngettext("%(num)s month", "%(num)s months", num=months)), 126 | (weeks, ngettext("%(num)s week", "%(num)s weeks", num=weeks)), 127 | (days, ngettext("%(num)s day", "%(num)s days", num=days)), 128 | (hours, ngettext("%(num)s hour", "%(num)s hours", num=hours)), 129 | (minutes, ngettext("%(num)s minute", "%(num)s minutes", num=minutes)), 130 | (seconds, ngettext("%(num)s second", "%(num)s seconds", num=seconds)), 131 | ) 132 | 133 | for period, trans in periods: 134 | if period: 135 | return gettext("%(period)s ago", period=trans) 136 | 137 | return default 138 | 139 | def domain(url): 140 | """ 141 | Returns the domain of a URL e.g. http://reddit.com/ > reddit.com 142 | """ 143 | rv = urlparse.urlparse(url).netloc 144 | if rv.startswith("www."): 145 | rv = rv[4:] 146 | return rv 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 | class Gravatar(object): 176 | """ 177 | Simple object for create gravatar link. 178 | 179 | gravatar = Gravatar( 180 | size=100, 181 | rating='g', 182 | default='retro', 183 | force_default=False, 184 | force_lower=False 185 | ) 186 | 187 | :param app: Your Flask app instance 188 | :param size: Default size for avatar 189 | :param rating: Default rating 190 | :param default: Default type for unregistred emails 191 | :param force_default: Build only default avatars 192 | :param force_lower: Make email.lower() before build link 193 | 194 | From flask-gravatar http://packages.python.org/Flask-Gravatar/ 195 | 196 | """ 197 | def __init__(self, size=100, rating='g', default='mm', 198 | force_default=False, force_lower=False): 199 | 200 | self.size = size 201 | self.rating = rating 202 | self.default = default 203 | self.force_default = force_default 204 | 205 | def __call__(self, email, size=None, rating=None, default=None, 206 | force_default=None, force_lower=False): 207 | 208 | """Build gravatar link.""" 209 | 210 | if size is None: 211 | size = self.size 212 | 213 | if rating is None: 214 | rating = self.rating 215 | 216 | if default is None: 217 | default = self.default 218 | 219 | if force_default is None: 220 | force_default = self.force_default 221 | 222 | if force_lower is None: 223 | force_lower = self.force_lower 224 | 225 | if force_lower: 226 | email = email.lower() 227 | 228 | hash = hashlib.md5(email).hexdigest() 229 | 230 | link = 'http://www.gravatar.com/avatar/{hash}'\ 231 | '?s={size}&d={default}&r={rating}'.format(**locals()) 232 | 233 | if force_default: 234 | link = link + '&f=y' 235 | 236 | return link 237 | 238 | gravatar = Gravatar() 239 | 240 | def gistcode(content): 241 | result = list(set(re.findall(r"(]*>\s*(https://gist.github.com/\d+)\s*)", content))) 242 | for i,link in result: 243 | content = content.replace(i, '%s ' % (i, link)) 244 | return content 245 | 246 | def code_highlight(value): 247 | f_list = _pre_re.findall(value) 248 | 249 | if f_list: 250 | s_list = _pre_re.split(value) 251 | 252 | for code_block in _pre_re.finditer(value): 253 | 254 | lang = _lang_re.search(code_block.group()).group('lang') 255 | code = code_block.group('code') 256 | 257 | index = s_list.index(code) 258 | s_list[index] = code2html(code, lang) 259 | 260 | return u''.join(s_list) 261 | 262 | return value 263 | 264 | def code2html(code, lang): 265 | lexer = get_lexer_by_name(lang, stripall=True) 266 | formatter = HtmlFormatter() 267 | return highlight(code, lexer, formatter) 268 | 269 | def ip2long(ip): 270 | return struct.unpack("!I",socket.inet_aton(ip))[0] 271 | 272 | def long2ip(num): 273 | return socket.inet_ntoa(struct.pack("!I",num)) 274 | 275 | -------------------------------------------------------------------------------- /pypress/themes/default/static/style.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Crimson+Text); 2 | body{ 3 | font: 0.75em/1.5em 'Georgia',Verdana,Arial,sans-serif; 4 | color: #333; 5 | background: #fff; 6 | } 7 | input,textarea,select{ 8 | font-size:12px; 9 | color:#333; 10 | } 11 | body,h1,h2,h3,h4,h5,h6,p,ul,ol{ 12 | margin:0; 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 | .clearfix:before,.clearfix:after { 31 | content:"."; 32 | display:block; 33 | height:0; 34 | overflow:hidden; 35 | } 36 | .clearfix:after, .clear { 37 | clear:both; 38 | } 39 | .clearfix{ 40 | zoom:1; /* IE < 8 */ 41 | } 42 | 43 | a { 44 | color: #06c; 45 | text-decoration: none; 46 | outline:none; 47 | } 48 | a:hover { 49 | background-color: #ffd; 50 | color: #069; 51 | text-decoration: underline; 52 | } 53 | 54 | a img { 55 | border: 0; 56 | } 57 | 58 | blockquote { 59 | color: #555; 60 | font-style: italic; 61 | font-size: 17px; 62 | } 63 | 64 | input.button { 65 | cursor: pointer; 66 | padding: 2px 10px; 67 | border: 1px solid #ccc; 68 | border-color: #aaa #aaa #666; 69 | -moz-border-radius: 5px; 70 | -khtml-border-radius: 5px; 71 | -webkit-border-radius: 5px; 72 | border-radius: 5px; 73 | -webkit-box-shadow:0 1px 3px #ddd; 74 | -moz-box-shadow: 0 1px 3px #ddd; 75 | color: #555; 76 | background: #f5f5f5 url("bg_button.png"); 77 | font-size: 14px; 78 | font-weight: bold; 79 | text-decoration: none; 80 | text-shadow: 0 1px 1px #fff; 81 | } 82 | input.button:hover { 83 | -webkit-box-shadow:0 1px 2px #eee; 84 | -moz-box-shadow: 0 1px 2px #eee; 85 | background-color: #fff; 86 | border-color: #ccc #ccc #999; 87 | } 88 | input.button::-moz-focus-inner{border-color:transparent!important;} 89 | input.text, 90 | textarea.text { 91 | margin-right: 10px; 92 | padding: 3px; 93 | border: 1px solid #ccc; 94 | border-color: #ccc; 95 | -moz-border-radius: 3px; 96 | -khtml-border-radius: 3px; 97 | -webkit-border-radius: 3px; 98 | border-radius: 3px; 99 | -webkit-box-shadow:0 0px 5px #ccc; 100 | -moz-box-shadow: 0 0px 5px #ccc; 101 | color: #555; 102 | font-size: 14px; 103 | } 104 | input.text:focus, 105 | textarea.text:focus { 106 | -webkit-box-shadow:0 0px 3px #aaa; 107 | -moz-box-shadow: 0 0px 3px #aaa; 108 | border-color: #aaa; 109 | } 110 | 111 | .errors { 112 | color: #f60; 113 | } 114 | 115 | #flashed { 116 | margin-bottom: 10px; 117 | padding: 5px; 118 | color: #f60; 119 | background: #eeefce; 120 | border: 1px solid #ccc; 121 | text-align: center; 122 | } 123 | 124 | #go-to-top { 125 | display: block; 126 | position: fixed; 127 | right: 60px; 128 | bottom: 100px; 129 | padding: 18px; 130 | color: #666; 131 | background: #ddd; 132 | font: 36px/18px Helvetica,Arial,Verdana,sans-serif; 133 | opacity: 0.7; 134 | outline: 0 none; 135 | text-decoration: none; 136 | text-shadow: 0 0 1px #ddd; 137 | vertical-align: baseline; 138 | -moz-border-radius: 5px; 139 | -khtml-border-radius: 5px; 140 | -webkit-border-radius: 5px; 141 | border-radius: 5px; 142 | } 143 | #header { 144 | padding: 10px 0; 145 | color: #eee; 146 | background: #666; 147 | } 148 | #header .head-inner { 149 | margin: auto; 150 | width: 950px; 151 | } 152 | #header h1 { 153 | display: inline; 154 | font-size: 16px; 155 | margin: 0 10px 0 0; 156 | color: #fff; 157 | line-height: 1em; 158 | } 159 | #header ul { 160 | list-style:none; 161 | } 162 | 163 | #footer { 164 | padding: 10px 0 20px; 165 | border-top: 1px solid #ddd; 166 | } 167 | #footer a { 168 | color: #666; 169 | } 170 | #footer p span { 171 | float: right; 172 | } 173 | 174 | #nav { 175 | float: right; 176 | } 177 | #nav li { 178 | display: inline; 179 | padding: 0 10px; 180 | } 181 | #nav li a:link { 182 | color: #fff; 183 | text-decoration: none; 184 | } 185 | #nav li a:visited { 186 | color: #ccc; 187 | } 188 | #nav li a:hover { 189 | color: #f90; 190 | text-decoration: underline; 191 | background: none; 192 | } 193 | #nav li a:active { 194 | color: #f90; 195 | } 196 | 197 | 198 | #container, #footer { 199 | margin: auto; 200 | width: 950px; 201 | } 202 | #container { 203 | padding: 30px 0; 204 | min-height: 420px; 205 | } 206 | 207 | #container .content { 208 | float: left; 209 | display: inline; 210 | padding-right: 20px; 211 | width: 680px; 212 | } 213 | #container .sidebar { 214 | float: right; 215 | display: inline; 216 | width: 220px; 217 | } 218 | 219 | #container h2.title { 220 | margin-bottom: 15px; 221 | border-bottom: 1px solid #ddd; 222 | } 223 | #container h2.error { 224 | border-bottom: 0; 225 | } 226 | 227 | .navigation-links { 228 | height: 2.2em; 229 | line-height: 2.2em; 230 | border: 1px solid #eee; 231 | border-width: 1px 0; 232 | overflow: hidden; 233 | } 234 | .navigation-links .previous { 235 | float: left; 236 | } 237 | .navigation-links .next { 238 | float: right; 239 | } 240 | 241 | #tab { 242 | clear: both; 243 | margin-bottom: 10px; 244 | padding-top: 20px; 245 | height: 26px; 246 | border-bottom: 1px solid #ccc; 247 | } 248 | #tab ul { 249 | padding: 0 20px; 250 | } 251 | #tab ul li { 252 | float: left; 253 | display: inline; 254 | margin: 0 2px; 255 | border: 1px solid #ccc; 256 | border-width: 1px 1px 0; 257 | height: 25px; 258 | line-height: 25px; 259 | background: #eee; 260 | } 261 | #tab ul li a { 262 | display: block; 263 | padding: 0 10px; 264 | color: #333; 265 | font-weight: bold; 266 | } 267 | #tab ul li a.selected { 268 | background: #fff; 269 | border-bottom: 1px solid #fff; 270 | } 271 | 272 | .post { 273 | clear: both; 274 | margin-bottom: 15px; 275 | padding: 0 0 15px; 276 | border-bottom: 1px solid #ddd; 277 | } 278 | .post-avatar { 279 | float: left; 280 | display: inline; 281 | margin: 0 10px 10px 0; 282 | border: 1px solid #ccc; 283 | width: 60px; 284 | height: 60px; 285 | overflow: hidden; 286 | } 287 | .post-avatar a { 288 | display: block; 289 | padding: 5px; 290 | } 291 | .post-title { 292 | line-height: 1.5em; 293 | } 294 | .post-title a { 295 | color: #555; 296 | } 297 | .post-title a:hover { 298 | color: #06c; 299 | background: none; 300 | text-decoration: none; 301 | } 302 | .post-byline, 303 | .post-tags { 304 | color: #888; 305 | } 306 | .post-byline a, 307 | .post-tags a { 308 | color: #666; 309 | text-decoration: underline; 310 | } 311 | .post-byline a:hover, 312 | .post-tags a:hover { 313 | color: #333; 314 | } 315 | 316 | .post-summary, .post-content { 317 | clear: both; 318 | font-size: 14px; 319 | } 320 | .post-summary p, .post-content p { 321 | margin: 0.85em 0; 322 | line-height: 1.8em; 323 | } 324 | .post-content ul, .post-content ol { 325 | margin-bottom: 1.2em; 326 | } 327 | .post-meta { 328 | color: #999; 329 | } 330 | .post-meta strong { 331 | color: #333; 332 | } 333 | 334 | .post-form th, 335 | .post-form td { 336 | padding: 5px; 337 | } 338 | .post-form input.button { 339 | padding: 5px 20px; 340 | } 341 | 342 | .sidebox { 343 | margin-bottom: 20px; 344 | position: relative; 345 | } 346 | 347 | .sidebox h3 { 348 | line-height: 2.2em; 349 | border-bottom: 1px solid #ddd; 350 | } 351 | .sidebox ul, 352 | .sidebox ol { 353 | padding: 0; 354 | } 355 | .sidebox li { 356 | list-style: none; 357 | line-height: 2em; 358 | border-bottom: 1px solid #eee; 359 | color: #999; 360 | } 361 | .sidebox strong { 362 | color: #333; 363 | } 364 | .sidebox small a { 365 | color: #333; 366 | } 367 | .sidebox .more { 368 | position: absolute; 369 | right: 5px; 370 | top: 5px; 371 | } 372 | #postnow input.button { 373 | margin: 0 auto 20px; 374 | padding: 5px 50px; 375 | font-size: 14px; 376 | } 377 | 378 | #post_twitter { 379 | clear: both; 380 | padding-top: 15px; 381 | } 382 | #post_twitter textarea { 383 | margin-bottom: 5px; 384 | padding:5px; 385 | width: 80%; 386 | font-size: 14px; 387 | border: 1px solid #ccc; 388 | -moz-border-radius: 5px; 389 | -khtml-border-radius: 5px; 390 | -webkit-border-radius: 5px; 391 | border-radius: 5px; 392 | -webkit-box-shadow:2px 2px 2px #ddd; 393 | -moz-box-shadow:2px 2px 2px #ddd; 394 | } 395 | #post_twitter textarea:focus { 396 | -webkit-box-shadow:0 0px 3px #aaa; 397 | -moz-box-shadow: 0 0px 3px #aaa; 398 | border-color: #aaa; 399 | } 400 | #post_twitter input.button { 401 | padding: 4px 20px; 402 | font-size: 14px; 403 | } 404 | .twitter ul { 405 | padding: 0; 406 | } 407 | .twitter ul li { 408 | list-style: none; 409 | margin-bottom: 10px; 410 | padding-bottom: 10px; 411 | border-bottom: 1px solid #eee; 412 | font-size: 14px; 413 | line-height: 1.5em; 414 | } 415 | .twitter ul li p.meta { 416 | font-size: 12px; 417 | color: #999; 418 | } 419 | .twitter ul li p.meta a { 420 | color: #555; 421 | } 422 | 423 | #searchform input.text { 424 | width: 136px; 425 | } 426 | #searchform p { 427 | line-height: 2em; 428 | } 429 | 430 | #user .avatar { 431 | float: left; 432 | display: inline; 433 | margin: 0 10px 0 0; 434 | } 435 | #user .info h3 { 436 | font-size: 16px; 437 | line-height: 1.8em; 438 | } 439 | #user .info p { 440 | color: #999; 441 | } 442 | 443 | .content #archive li, 444 | .content #links li { 445 | padding: 5px 0; 446 | line-height: 1.4em; 447 | } 448 | .content #links li p { 449 | padding: 5px 0; 450 | color: #999; 451 | } 452 | 453 | #links small { 454 | float: right; 455 | } 456 | #tags li small { 457 | float: right; 458 | } 459 | #tags li small a { 460 | color: #999; 461 | } 462 | #tag-cloud ul { 463 | padding: 0; 464 | } 465 | #tag-cloud li { 466 | display: inline; 467 | padding: 0 5px; 468 | } 469 | a.tag-0 { color:#aaa; font-size: 0.9em; font-weight: 100; } 470 | a.tag-1 { color:#999; font-size: 0.9em; font-weight: 100; } 471 | a.tag-2 { color:#999; font-size: 1em; font-weight: 200; } 472 | a.tag-3 { color:#888; font-size: 1.1em; font-weight: 300; } 473 | a.tag-4 { color:#777; font-size: 1.2em; font-weight: 400; } 474 | a.tag-5 { color:#666; font-size: 1.3em; font-weight: 500; } 475 | a.tag-6 { color:#555; font-size: 1.4em; font-weight: 600; } 476 | a.tag-7 { color:#333; font-size: 1.6em; font-weight: 700; } 477 | a.tag-8 { color:#000; font-size: 1.8em; font-weight: 800; } 478 | a.tag-9 { color:#06c; font-size: 2em; font-weight: 900; } 479 | a.tag-10 { color:#c60; font-size: 2em; font-weight: 900; } 480 | 481 | #comments { 482 | padding: 10px 0; 483 | } 484 | .comment-add { 485 | margin-bottom: 20px; 486 | } 487 | .comment-add h3 { 488 | padding: 10px 0; 489 | } 490 | .comment-add p { 491 | margin: 5px 0; 492 | } 493 | .comment-add input[type=text] { 494 | margin-right: 5px; 495 | padding: 2px; 496 | width: 200px; 497 | } 498 | .comment-add textarea { 499 | padding: 2px; 500 | width: 60%; 501 | height: 80px; 502 | } 503 | .comment-list ol { 504 | list-style: none; 505 | padding: 0; 506 | } 507 | .comment-list ol ol { 508 | margin-left: 70px; 509 | } 510 | .comment { 511 | clear: both; 512 | overflow: auto; 513 | margin: 15px 0; 514 | padding-top: 15px; 515 | width: 100%; 516 | border-top: 1px solid #ddd; 517 | } 518 | .comment-avatar { 519 | display: block; 520 | float: left; 521 | height: 60px; 522 | margin: 0 10px 0 0; 523 | overflow: hidden; 524 | position: relative; 525 | width: 60px; 526 | border: 1px solid #ddd; 527 | } 528 | .comment-avatar a { 529 | display: block; 530 | padding: 5px; 531 | } 532 | .comment form { 533 | clear: both; 534 | display: block; 535 | position: absolute; 536 | margin: 5px 0 0 0; 537 | z-index: 999; 538 | padding: 5px; 539 | width: 300px; 540 | border: 1px solid #ddd; 541 | background: #eee; 542 | } 543 | .comment form textarea { 544 | width: 290px; 545 | height: 50px; 546 | } 547 | .comment-text { 548 | margin: 0 0 0 72px; 549 | } 550 | 551 | .comment-add input.button { 552 | padding: 3px 15px; 553 | } 554 | 555 | .form-table th, 556 | .form-table td { 557 | padding: 5px; 558 | } 559 | .form-table th { 560 | text-align: left; 561 | } 562 | .form-table input.button { 563 | padding: 3px 15px; 564 | } 565 | 566 | .error h1 { 567 | font-family: 'Crimson Text','Georgia',serif; 568 | line-height: 2em; 569 | font-size: 30px; 570 | font-weight: normal; 571 | color: #000; 572 | } 573 | .error .simple p { 574 | font-size: 14px; 575 | line-height: 2em; 576 | } 577 | .error form input.text { 578 | width: 300px; 579 | } 580 | 581 | -------------------------------------------------------------------------------- /messages.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2011 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2011. 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: 2011-03-12 14:59+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.5\n" 19 | 20 | #: pypress/__init__.py:193 21 | msgid "Login required" 22 | msgstr "" 23 | 24 | #: pypress/__init__.py:194 25 | msgid "Please login to see this page" 26 | msgstr "" 27 | 28 | #: pypress/__init__.py:200 29 | msgid "Sorry, page not allowed" 30 | msgstr "" 31 | 32 | #: pypress/__init__.py:206 33 | msgid "Sorry, page not found" 34 | msgstr "" 35 | 36 | #: pypress/__init__.py:212 37 | msgid "Sorry, an error has occurred" 38 | msgstr "" 39 | 40 | #: pypress/helpers.py:107 41 | msgid "just now" 42 | msgstr "" 43 | 44 | #: pypress/helpers.py:121 45 | #, python-format 46 | msgid "%(num)s year" 47 | msgid_plural "%(num)s years" 48 | msgstr[0] "" 49 | msgstr[1] "" 50 | 51 | #: pypress/helpers.py:122 52 | #, python-format 53 | msgid "%(num)s month" 54 | msgid_plural "%(num)s months" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | #: pypress/helpers.py:123 59 | #, python-format 60 | msgid "%(num)s week" 61 | msgid_plural "%(num)s weeks" 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | #: pypress/helpers.py:124 66 | #, python-format 67 | msgid "%(num)s day" 68 | msgid_plural "%(num)s days" 69 | msgstr[0] "" 70 | msgstr[1] "" 71 | 72 | #: pypress/helpers.py:125 73 | #, python-format 74 | msgid "%(num)s hour" 75 | msgid_plural "%(num)s hours" 76 | msgstr[0] "" 77 | msgstr[1] "" 78 | 79 | #: pypress/helpers.py:126 80 | #, python-format 81 | msgid "%(num)s minute" 82 | msgid_plural "%(num)s minutes" 83 | msgstr[0] "" 84 | msgstr[1] "" 85 | 86 | #: pypress/helpers.py:127 87 | #, python-format 88 | msgid "%(num)s second" 89 | msgid_plural "%(num)s seconds" 90 | msgstr[0] "" 91 | msgstr[1] "" 92 | 93 | #: pypress/helpers.py:132 94 | #, python-format 95 | msgid "%(period)s ago" 96 | msgstr "" 97 | 98 | #: pypress/forms/account.py:23 99 | msgid "Username or email address" 100 | msgstr "" 101 | 102 | #: pypress/forms/account.py:25 103 | msgid "You must provide an email or username" 104 | msgstr "" 105 | 106 | #: pypress/forms/account.py:27 pypress/forms/account.py:45 107 | #: pypress/forms/account.py:83 108 | msgid "Password" 109 | msgstr "" 110 | 111 | #: pypress/forms/account.py:29 112 | msgid "Remember me" 113 | msgstr "" 114 | 115 | #: pypress/forms/account.py:33 pypress/themes/default/templates/layout.html:48 116 | msgid "Login" 117 | msgstr "" 118 | 119 | #: pypress/forms/account.py:38 120 | msgid "Username" 121 | msgstr "" 122 | 123 | #: pypress/forms/account.py:39 124 | msgid "Username required" 125 | msgstr "" 126 | 127 | #: pypress/forms/account.py:42 pypress/forms/blog.py:55 128 | msgid "Nickname" 129 | msgstr "" 130 | 131 | #: pypress/forms/account.py:43 pypress/forms/blog.py:56 132 | msgid "Nickname required" 133 | msgstr "" 134 | 135 | #: pypress/forms/account.py:46 136 | msgid "Password required" 137 | msgstr "" 138 | 139 | #: pypress/forms/account.py:48 pypress/forms/account.py:89 140 | msgid "Password again" 141 | msgstr "" 142 | 143 | #: pypress/forms/account.py:50 pypress/forms/account.py:91 144 | msgid "Passwords don't match" 145 | msgstr "" 146 | 147 | #: pypress/forms/account.py:52 148 | msgid "Email address" 149 | msgstr "" 150 | 151 | #: pypress/forms/account.py:53 152 | msgid "Email address required" 153 | msgstr "" 154 | 155 | #: pypress/forms/account.py:54 pypress/forms/account.py:76 156 | #: pypress/forms/blog.py:53 157 | msgid "A valid email address is required" 158 | msgstr "" 159 | 160 | #: pypress/forms/account.py:56 161 | msgid "Signup Code" 162 | msgstr "" 163 | 164 | #: pypress/forms/account.py:60 pypress/templates/errors/403.html:15 165 | msgid "Signup" 166 | msgstr "" 167 | 168 | #: pypress/forms/account.py:65 169 | msgid "This username is taken" 170 | msgstr "" 171 | 172 | #: pypress/forms/account.py:70 173 | msgid "This email is taken" 174 | msgstr "" 175 | 176 | #: pypress/forms/account.py:78 177 | msgid "Find password" 178 | msgstr "" 179 | 180 | #: pypress/forms/account.py:84 181 | msgid "Password is required" 182 | msgstr "" 183 | 184 | #: pypress/forms/account.py:86 185 | msgid "New Password" 186 | msgstr "" 187 | 188 | #: pypress/forms/account.py:87 189 | msgid "New Password is required" 190 | msgstr "" 191 | 192 | #: pypress/forms/account.py:93 pypress/forms/blog.py:31 193 | #: pypress/forms/blog.py:86 pypress/forms/blog.py:94 194 | msgid "Save" 195 | msgstr "" 196 | 197 | #: pypress/forms/account.py:98 198 | msgid "Recaptcha" 199 | msgstr "" 200 | 201 | #: pypress/forms/account.py:100 202 | msgid "Delete" 203 | msgstr "" 204 | 205 | #: pypress/forms/account.py:105 pypress/forms/blog.py:26 206 | msgid "Content" 207 | msgstr "" 208 | 209 | #: pypress/forms/account.py:106 210 | msgid "Content is required" 211 | msgstr "" 212 | 213 | #: pypress/forms/account.py:108 214 | msgid "Send" 215 | msgstr "" 216 | 217 | #: pypress/forms/blog.py:21 218 | msgid "Title" 219 | msgstr "" 220 | 221 | #: pypress/forms/blog.py:22 222 | msgid "Title required" 223 | msgstr "" 224 | 225 | #: pypress/forms/blog.py:24 226 | msgid "Slug" 227 | msgstr "" 228 | 229 | #: pypress/forms/blog.py:28 pypress/templates/blog/_tags.html:2 230 | #: pypress/templates/blog/tags.html:7 231 | #: pypress/themes/default/templates/layout.html:30 232 | msgid "Tags" 233 | msgstr "" 234 | 235 | #: pypress/forms/blog.py:29 236 | msgid "Tags required" 237 | msgstr "" 238 | 239 | #: pypress/forms/blog.py:39 240 | msgid "Slug must be less than 50 characters" 241 | msgstr "" 242 | 243 | #: pypress/forms/blog.py:45 244 | msgid "This slug is taken" 245 | msgstr "" 246 | 247 | #: pypress/forms/blog.py:45 248 | msgid "Slug is required" 249 | msgstr "" 250 | 251 | #: pypress/forms/blog.py:51 pypress/forms/blog.py:77 252 | msgid "Email" 253 | msgstr "" 254 | 255 | #: pypress/forms/blog.py:52 256 | msgid "Email required" 257 | msgstr "" 258 | 259 | #: pypress/forms/blog.py:58 260 | msgid "Website" 261 | msgstr "" 262 | 263 | #: pypress/forms/blog.py:60 pypress/forms/blog.py:75 pypress/forms/blog.py:82 264 | msgid "A valid url is required" 265 | msgstr "" 266 | 267 | #: pypress/forms/blog.py:62 268 | msgid "Comment" 269 | msgstr "" 270 | 271 | #: pypress/forms/blog.py:63 272 | msgid "Comment required" 273 | msgstr "" 274 | 275 | #: pypress/forms/blog.py:65 276 | msgid "Add comment" 277 | msgstr "" 278 | 279 | #: pypress/forms/blog.py:66 pypress/forms/blog.py:95 280 | msgid "Cancel" 281 | msgstr "" 282 | 283 | #: pypress/forms/blog.py:71 284 | msgid "Site name" 285 | msgstr "" 286 | 287 | #: pypress/forms/blog.py:72 288 | msgid "Name required" 289 | msgstr "" 290 | 291 | #: pypress/forms/blog.py:74 292 | msgid "link" 293 | msgstr "" 294 | 295 | #: pypress/forms/blog.py:78 296 | msgid "A valid email is required" 297 | msgstr "" 298 | 299 | #: pypress/forms/blog.py:80 300 | msgid "Logo" 301 | msgstr "" 302 | 303 | #: pypress/forms/blog.py:84 304 | msgid "Description" 305 | msgstr "" 306 | 307 | #: pypress/forms/blog.py:91 308 | msgid "HTML" 309 | msgstr "" 310 | 311 | #: pypress/forms/blog.py:92 312 | msgid "HTML required" 313 | msgstr "" 314 | 315 | #: pypress/forms/validators.py:17 316 | msgid "You can only use letters, numbers or dashes" 317 | msgstr "" 318 | 319 | #: pypress/models/blog.py:205 320 | msgid "Read more..." 321 | msgstr "" 322 | 323 | #: pypress/templates/account/login.html:5 pypress/templates/errors/403.html:14 324 | msgid "Login to pypress" 325 | msgstr "" 326 | 327 | #: pypress/templates/account/login.html:31 328 | msgid "Not a member yet ? Sign up !" 329 | msgstr "" 330 | 331 | #: pypress/templates/account/signup.html:6 332 | msgid "Sign up to pypress" 333 | msgstr "" 334 | 335 | #: pypress/templates/blog/_archive.html:2 pypress/templates/blog/archive.html:7 336 | #: pypress/themes/default/templates/layout.html:29 337 | msgid "Archive" 338 | msgstr "" 339 | 340 | #: pypress/templates/blog/_links.html:2 pypress/templates/blog/links.html:7 341 | msgid "Links" 342 | msgstr "" 343 | 344 | #: pypress/templates/blog/_links.html:11 pypress/templates/blog/add_link.html:7 345 | msgid "Add link" 346 | msgstr "" 347 | 348 | #: pypress/templates/blog/_links.html:12 349 | msgid "More" 350 | msgstr "" 351 | 352 | #: pypress/templates/blog/_postnow.html:2 353 | #: pypress/themes/default/templates/layout.html:32 354 | msgid "Post Now" 355 | msgstr "" 356 | 357 | #: pypress/templates/blog/_search.html:4 358 | msgid "Search all posts" 359 | msgstr "" 360 | 361 | #: pypress/templates/blog/_search.html:5 pypress/templates/errors/404.html:14 362 | msgid "Search" 363 | msgstr "" 364 | 365 | #: pypress/templates/blog/about.html:5 366 | #: pypress/themes/default/templates/layout.html:31 367 | msgid "About" 368 | msgstr "" 369 | 370 | #: pypress/templates/blog/links.html:15 371 | msgid "pass" 372 | msgstr "" 373 | 374 | #: pypress/templates/blog/links.html:18 pypress/templates/macros/_post.html:32 375 | msgid "delete" 376 | msgstr "" 377 | 378 | #: pypress/templates/blog/links.html:20 379 | msgid "Are you sure you want to delete this link ?" 380 | msgstr "" 381 | 382 | #: pypress/templates/blog/links.html:21 pypress/templates/blog/view.html:30 383 | #: pypress/templates/macros/_post.html:35 384 | msgid "yes" 385 | msgstr "" 386 | 387 | #: pypress/templates/blog/links.html:22 pypress/templates/blog/view.html:31 388 | #: pypress/templates/macros/_post.html:36 389 | msgid "no" 390 | msgstr "" 391 | 392 | #: pypress/templates/blog/list.html:23 393 | msgid "Modified at" 394 | msgstr "" 395 | 396 | #: pypress/templates/blog/list.html:30 397 | msgid "Nobody's posted anything yet." 398 | msgstr "" 399 | 400 | #: pypress/templates/blog/people.html:12 401 | msgid "Joined in" 402 | msgstr "" 403 | 404 | #: pypress/templates/blog/people.html:28 405 | msgid "Posts" 406 | msgstr "" 407 | 408 | #: pypress/templates/blog/people.html:29 409 | msgid "Tweets" 410 | msgstr "" 411 | 412 | #: pypress/templates/blog/people.html:39 413 | msgid "Posted at" 414 | msgstr "" 415 | 416 | #: pypress/templates/blog/people.html:47 417 | msgid "You has not posted anything yet." 418 | msgstr "" 419 | 420 | #: pypress/templates/blog/people.html:47 421 | msgid "Submit" 422 | msgstr "" 423 | 424 | #: pypress/templates/blog/people.html:49 425 | #, python-format 426 | msgid "%(name)s has not posted anything yet." 427 | msgstr "" 428 | 429 | #: pypress/templates/blog/search_result.html:8 430 | msgid "Search results for" 431 | msgstr "" 432 | 433 | #: pypress/templates/blog/search_result.html:20 434 | msgid "No posts found. Try a different search." 435 | msgstr "" 436 | 437 | #: pypress/templates/blog/submit.html:7 438 | msgid "Submit a post" 439 | msgstr "" 440 | 441 | #: pypress/templates/blog/submit.html:86 442 | msgid "Normal" 443 | msgstr "" 444 | 445 | #: pypress/templates/blog/submit.html:95 446 | msgid "Large" 447 | msgstr "" 448 | 449 | #: pypress/templates/blog/submit.html:104 450 | msgid "Maximum" 451 | msgstr "" 452 | 453 | #: pypress/templates/blog/template_edit.html:8 454 | msgid "Edit template" 455 | msgstr "" 456 | 457 | #: pypress/templates/blog/view.html:21 458 | msgid "Modified at " 459 | msgstr "" 460 | 461 | #: pypress/templates/blog/view.html:24 462 | msgid "edit this post" 463 | msgstr "" 464 | 465 | #: pypress/templates/blog/view.html:27 466 | msgid "delete this post" 467 | msgstr "" 468 | 469 | #: pypress/templates/blog/view.html:29 470 | msgid "Are you sure you want to delete this post ?" 471 | msgstr "" 472 | 473 | #: pypress/templates/blog/view.html:46 474 | msgid "Add a comment" 475 | msgstr "" 476 | 477 | #: pypress/templates/blog/view.html:64 478 | msgid "Comments" 479 | msgstr "" 480 | 481 | #: pypress/templates/blog/view.html:72 482 | msgid "No comments have been posted yet." 483 | msgstr "" 484 | 485 | #: pypress/templates/errors/403.html:5 486 | msgid "403, Page Not Allowed" 487 | msgstr "" 488 | 489 | #: pypress/templates/errors/403.html:12 490 | msgid "Maybe we can help you out though:" 491 | msgstr "" 492 | 493 | #: pypress/templates/errors/403.html:15 494 | msgid "Not a member yet?" 495 | msgstr "" 496 | 497 | #: pypress/templates/errors/404.html:5 498 | msgid "404, Page Not Found" 499 | msgstr "" 500 | 501 | #: pypress/templates/errors/404.html:12 502 | msgid "Maybe you can try search:" 503 | msgstr "" 504 | 505 | #: pypress/templates/errors/500.html:5 506 | msgid "500, An Error Has Occurred" 507 | msgstr "" 508 | 509 | #: pypress/templates/macros/_page.html:7 510 | msgid "newer" 511 | msgstr "" 512 | 513 | #: pypress/templates/macros/_page.html:13 514 | msgid "older" 515 | msgstr "" 516 | 517 | #: pypress/templates/macros/_post.html:9 518 | msgid "permalink" 519 | msgstr "" 520 | 521 | #: pypress/templates/macros/_post.html:18 522 | msgid "reply" 523 | msgstr "" 524 | 525 | #: pypress/templates/macros/_post.html:34 526 | msgid "Are you sure you want to delete this comment ?" 527 | msgstr "" 528 | 529 | #: pypress/themes/default/templates/layout.html:28 530 | msgid "Home" 531 | msgstr "" 532 | 533 | #: pypress/themes/default/templates/layout.html:48 534 | msgid "Logout" 535 | msgstr "" 536 | 537 | #: pypress/views/account.py:59 538 | #, python-format 539 | msgid "Welcome back, %(name)s" 540 | msgstr "" 541 | 542 | #: pypress/views/account.py:70 543 | msgid "Sorry, invalid login" 544 | msgstr "" 545 | 546 | #: pypress/views/account.py:95 547 | #, python-format 548 | msgid "Welcome, %(name)s" 549 | msgstr "" 550 | 551 | #: pypress/views/account.py:104 552 | msgid "Code is not allowed" 553 | msgstr "" 554 | 555 | #: pypress/views/account.py:112 556 | msgid "You are now logged out" 557 | msgstr "" 558 | 559 | #: pypress/views/account.py:129 560 | msgid "You twitter's access token is already exists" 561 | msgstr "" 562 | 563 | #: pypress/views/account.py:178 564 | msgid "Twitter request success" 565 | msgstr "" 566 | 567 | #: pypress/views/frontend.py:130 568 | msgid "Twitter posting is success" 569 | msgstr "" 570 | 571 | #: pypress/views/frontend.py:132 572 | msgid "Twitter posting is failed" 573 | msgstr "" 574 | 575 | #: pypress/views/frontend.py:156 576 | msgid "Please select a picture" 577 | msgstr "" 578 | 579 | #: pypress/views/frontend.py:208 580 | msgid "Template file does not exists" 581 | msgstr "" 582 | 583 | #: pypress/views/frontend.py:218 584 | msgid "Saving success" 585 | msgstr "" 586 | 587 | #: pypress/views/link.py:57 588 | msgid "Adding success" 589 | msgstr "" 590 | 591 | #: pypress/views/post.py:44 592 | msgid "Posting success" 593 | msgstr "" 594 | 595 | #: pypress/views/post.py:77 596 | msgid "Post has been changed" 597 | msgstr "" 598 | 599 | #: pypress/views/post.py:106 600 | msgid "The post has been deleted" 601 | msgstr "" 602 | 603 | #: pypress/views/post.py:137 604 | msgid "Thanks for your comment" 605 | msgstr "" 606 | 607 | -------------------------------------------------------------------------------- /pypress/models/blog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | models: blog.py 5 | ~~~~~~~~~~~~~ 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | import hashlib, re, random 10 | 11 | from datetime import datetime 12 | 13 | from werkzeug import cached_property 14 | 15 | from flask import abort, current_app, url_for, Markup 16 | 17 | from flaskext.babel import gettext as _ 18 | from flaskext.sqlalchemy import BaseQuery 19 | from flaskext.principal import RoleNeed, UserNeed, Permission 20 | 21 | from pypress import signals 22 | from pypress.helpers import storage, slugify, markdown 23 | 24 | from pypress.extensions import db 25 | from pypress.permissions import moderator, admin 26 | 27 | from pypress.models.users import User 28 | 29 | class PostQuery(BaseQuery): 30 | 31 | def jsonify(self): 32 | for post in self.all(): 33 | yield post.json 34 | 35 | def as_list(self): 36 | """ 37 | Return restricted list of columns for list queries 38 | """ 39 | 40 | deferred_cols = ("content", 41 | "tags", 42 | "author.email", 43 | "author.activation_key", 44 | "author.date_joined", 45 | "author.last_login", 46 | "author.last_request") 47 | 48 | options = [db.defer(col) for col in deferred_cols] 49 | return self.options(*options) 50 | 51 | def get_by_slug(self, slug): 52 | post = self.filter(Post.slug==slug).first() 53 | if post is None: 54 | abort(404) 55 | return post 56 | 57 | def search(self, keywords): 58 | 59 | criteria = [] 60 | 61 | for keyword in keywords.split(): 62 | keyword = '%' + keyword + '%' 63 | criteria.append(db.or_(Post.title.ilike(keyword), 64 | Post.content.ilike(keyword), 65 | Post.tags.ilike(keyword) 66 | )) 67 | 68 | q = reduce(db.and_, criteria) 69 | return self.filter(q) 70 | 71 | def archive(self, year, month, day): 72 | if not year: 73 | return self 74 | 75 | criteria = [] 76 | criteria.append(db.extract('year',Post.created_date)==year) 77 | if month: criteria.append(db.extract('month',Post.created_date)==month) 78 | if day: criteria.append(db.extract('day',Post.created_date)==day) 79 | 80 | q = reduce(db.and_, criteria) 81 | return self.filter(q) 82 | 83 | 84 | class Post(db.Model): 85 | 86 | __tablename__ = 'posts' 87 | 88 | PER_PAGE = 40 89 | 90 | query_class = PostQuery 91 | 92 | id = db.Column(db.Integer, primary_key=True) 93 | 94 | author_id = db.Column(db.Integer, 95 | db.ForeignKey(User.id, ondelete='CASCADE'), 96 | nullable=False) 97 | 98 | _title = db.Column("title", db.Unicode(100), index=True) 99 | _slug = db.Column("slug", db.Unicode(50), unique=True, index=True) 100 | content = db.Column(db.UnicodeText) 101 | num_comments = db.Column(db.Integer, default=0) 102 | created_date = db.Column(db.DateTime, default=datetime.utcnow) 103 | update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 104 | 105 | _tags = db.Column("tags", db.Unicode(100), index=True) 106 | 107 | author = db.relation(User, innerjoin=True, lazy="joined") 108 | 109 | __mapper_args__ = {'order_by': id.desc()} 110 | 111 | class Permissions(object): 112 | 113 | def __init__(self, obj): 114 | self.obj = obj 115 | 116 | @cached_property 117 | def edit(self): 118 | return Permission(UserNeed(self.obj.author_id)) 119 | 120 | @cached_property 121 | def delete(self): 122 | return Permission(UserNeed(self.obj.author_id)) & moderator 123 | 124 | def __init__(self, *args, **kwargs): 125 | super(Post, self).__init__(*args, **kwargs) 126 | 127 | def __str__(self): 128 | return self.title 129 | 130 | def __repr__(self): 131 | return "<%s>" % self 132 | 133 | @cached_property 134 | def permissions(self): 135 | return self.Permissions(self) 136 | 137 | def _get_title(self): 138 | return self._title 139 | 140 | def _set_title(self, title): 141 | self._title = title.lower().strip() 142 | if self.slug is None: 143 | self.slug = slugify(title)[:50] 144 | 145 | title = db.synonym("_title", descriptor=property(_get_title, _set_title)) 146 | 147 | def _get_slug(self): 148 | return self._slug 149 | 150 | def _set_slug(self, slug): 151 | if slug: 152 | self._slug = slugify(slug) 153 | 154 | slug = db.synonym("_slug", descriptor=property(_get_slug, _set_slug)) 155 | 156 | def _get_tags(self): 157 | return self._tags 158 | 159 | def _set_tags(self, tags): 160 | 161 | self._tags = tags 162 | 163 | if self.id: 164 | # ensure existing tag references are removed 165 | d = db.delete(post_tags, post_tags.c.post_id==self.id) 166 | db.engine.execute(d) 167 | 168 | for tag in set(self.taglist): 169 | 170 | slug = slugify(tag) 171 | 172 | tag_obj = Tag.query.filter(Tag.slug==slug).first() 173 | if tag_obj is None: 174 | tag_obj = Tag(name=tag, slug=slug) 175 | db.session.add(tag_obj) 176 | 177 | tag_obj.posts.append(self) 178 | 179 | tags = db.synonym("_tags", descriptor=property(_get_tags, _set_tags)) 180 | 181 | @property 182 | def taglist(self): 183 | if self.tags is None: 184 | return [] 185 | 186 | tags = [t.strip() for t in self.tags.split(",")] 187 | return [t for t in tags if t] 188 | 189 | @cached_property 190 | def linked_taglist(self): 191 | """ 192 | Returns the tags in the original order and format, 193 | with link to tag page 194 | """ 195 | return [(tag, url_for('frontend.tag', 196 | slug=slugify(tag))) \ 197 | for tag in self.taglist] 198 | 199 | @cached_property 200 | def summary(self): 201 | s = re.findall(r'(

    )', self.content) 202 | if not s: 203 | return self.content 204 | p, more_id = s[0] 205 | addlink = '

    %s

    ' % (self.url, more_id, _("Read more...")) 206 | return self.content.split(p)[0] + addlink 207 | 208 | @cached_property 209 | def comments(self): 210 | """ 211 | Returns comments in tree. Each parent comment has a "comments" 212 | attribute appended and a "depth" attribute. 213 | """ 214 | comments = Comment.query.filter(Comment.post_id==self.id).all() 215 | 216 | def _get_comments(parent, depth): 217 | 218 | parent.comments = [] 219 | parent.depth = depth 220 | 221 | for comment in comments: 222 | if comment.parent_id == parent.id: 223 | parent.comments.append(comment) 224 | _get_comments(comment, depth + 1) 225 | 226 | parents = [c for c in comments if c.parent_id is None] 227 | 228 | for parent in parents: 229 | _get_comments(parent, 0) 230 | 231 | return parents 232 | 233 | @cached_property 234 | def json(self): 235 | """ 236 | Returns dict of safe attributes for passing into 237 | a JSON request. 238 | """ 239 | 240 | return dict(id=self.id, 241 | title=self.title, 242 | content=self.content, 243 | author=self.author.username) 244 | 245 | def _url(self, _external=False): 246 | return url_for('frontend.post', 247 | year=self.created_date.year, 248 | month=self.created_date.month, 249 | day=self.created_date.day, 250 | slug=self.slug, 251 | _external=_external) 252 | 253 | @cached_property 254 | def url(self): 255 | return self._url() 256 | 257 | @cached_property 258 | def permalink(self): 259 | return self._url(True) 260 | 261 | 262 | post_tags = db.Table("post_tags", db.Model.metadata, 263 | db.Column("post_id", db.Integer, 264 | db.ForeignKey('posts.id', ondelete='CASCADE'), 265 | primary_key=True), 266 | db.Column("tag_id", db.Integer, 267 | db.ForeignKey('tags.id', ondelete='CASCADE'), 268 | primary_key=True)) 269 | 270 | 271 | class TagQuery(BaseQuery): 272 | 273 | def cloud(self): 274 | 275 | tags = self.filter(Tag.num_posts > 0).all() 276 | 277 | if not tags: 278 | return [] 279 | 280 | max_posts = max(t.num_posts for t in tags) 281 | min_posts = min(t.num_posts for t in tags) 282 | 283 | diff = (max_posts - min_posts) / 10.0 284 | if diff < 0.1: 285 | diff = 0.1 286 | 287 | for tag in tags: 288 | tag.size = int(tag.num_posts / diff) 289 | if tag.size < 1: 290 | tag.size = 1 291 | 292 | random.shuffle(tags) 293 | 294 | return tags 295 | 296 | 297 | class Tag(db.Model): 298 | 299 | __tablename__ = "tags" 300 | 301 | query_class = TagQuery 302 | 303 | id = db.Column(db.Integer, primary_key=True) 304 | slug = db.Column(db.Unicode(80), unique=True) 305 | posts = db.dynamic_loader(Post, secondary=post_tags, query_class=PostQuery) 306 | 307 | _name = db.Column("name", db.Unicode(80), unique=True) 308 | 309 | def __init__(self, *args, **kwargs): 310 | super(Tag, self).__init__(*args, **kwargs) 311 | 312 | def __str__(self): 313 | return self.name 314 | 315 | def _get_name(self): 316 | return self._name 317 | 318 | def _set_name(self, name): 319 | self._name = name.lower().strip() 320 | self.slug = slugify(name) 321 | 322 | name = db.synonym("_name", descriptor=property(_get_name, _set_name)) 323 | 324 | @cached_property 325 | def url(self): 326 | return url_for("frontend.tag", slug=self.slug) 327 | 328 | num_posts = db.column_property( 329 | db.select([db.func.count(post_tags.c.post_id)]).\ 330 | where(db.and_(post_tags.c.tag_id==id, 331 | Post.id==post_tags.c.post_id)).as_scalar()) 332 | 333 | 334 | class Comment(db.Model): 335 | 336 | __tablename__ = "comments" 337 | 338 | PER_PAGE = 40 339 | 340 | id = db.Column(db.Integer, primary_key=True) 341 | 342 | post_id = db.Column(db.Integer, 343 | db.ForeignKey(Post.id, ondelete='CASCADE'), 344 | nullable=False) 345 | 346 | author_id = db.Column(db.Integer, 347 | db.ForeignKey(User.id, ondelete='CASCADE')) 348 | 349 | parent_id = db.Column(db.Integer, 350 | db.ForeignKey("comments.id", ondelete='CASCADE')) 351 | 352 | email = db.Column(db.String(50)) 353 | nickname = db.Column(db.Unicode(50)) 354 | website = db.Column(db.String(100)) 355 | 356 | comment = db.Column(db.UnicodeText) 357 | created_date = db.Column(db.DateTime, default=datetime.utcnow) 358 | 359 | ip = db.Column(db.Integer) 360 | 361 | _author = db.relation(User, backref="posts", lazy="joined") 362 | 363 | post = db.relation(Post, innerjoin=True, lazy="joined") 364 | 365 | parent = db.relation('Comment', remote_side=[id]) 366 | 367 | __mapper_args__ = {'order_by' : id.asc()} 368 | 369 | class Permissions(object): 370 | 371 | def __init__(self, obj): 372 | self.obj = obj 373 | 374 | @cached_property 375 | def reply(self): 376 | return Permission(UserNeed(self.obj.post.author_id)) 377 | 378 | @cached_property 379 | def delete(self): 380 | return Permission(UserNeed(self.obj.author_id), 381 | UserNeed(self.obj.post.author_id)) & moderator 382 | 383 | def __init__(self, *args, **kwargs): 384 | super(Comment, self).__init__(*args, **kwargs) 385 | 386 | @cached_property 387 | def permissions(self): 388 | return self.Permissions(self) 389 | 390 | def _get_author(self): 391 | if self._author: 392 | return self._author 393 | return storage(email = self.email, 394 | nickname = self.nickname, 395 | website = self.website) 396 | 397 | def _set_author(self, author): 398 | self._author = author 399 | 400 | author = db.synonym("_author", descriptor=property(_get_author, _set_author)) 401 | 402 | def _url(self, _external=False): 403 | return '%s#comment-%d' % (self.post._url(_external), self.id) 404 | 405 | @cached_property 406 | def url(self): 407 | return self._url() 408 | 409 | @cached_property 410 | def permalink(self): 411 | return self._url(True) 412 | 413 | @cached_property 414 | def markdown(self): 415 | return Markup(markdown(self.comment or '')) 416 | 417 | 418 | class Link(db.Model): 419 | 420 | __tablename__ = "links" 421 | 422 | PER_PAGE = 80 423 | 424 | id = db.Column(db.Integer, primary_key=True) 425 | name = db.Column(db.Unicode(50), nullable=False) 426 | link = db.Column(db.String(100), nullable=False) 427 | logo = db.Column(db.String(100)) 428 | description = db.Column(db.Unicode(100)) 429 | email = db.Column(db.String(50)) 430 | passed = db.Column(db.Boolean, default=False) 431 | created_date = db.Column(db.DateTime, default=datetime.utcnow) 432 | 433 | class Permissions(object): 434 | 435 | def __init__(self, obj): 436 | self.obj = obj 437 | 438 | @cached_property 439 | def edit(self): 440 | return moderator 441 | 442 | @cached_property 443 | def delete(self): 444 | return moderator 445 | 446 | def __init__(self, *args, **kwargs): 447 | super(Link, self).__init__(*args, **kwargs) 448 | 449 | @cached_property 450 | def permissions(self): 451 | return self.Permissions(self) 452 | 453 | def __str__(self): 454 | return self.name 455 | 456 | # ------------- SIGNALS ----------------# 457 | 458 | def update_num_comments(sender): 459 | sender.num_comments = \ 460 | Comment.query.filter(Comment.post_id==sender.id).count() 461 | db.session.commit() 462 | 463 | 464 | signals.comment_added.connect(update_num_comments) 465 | signals.comment_deleted.connect(update_num_comments) 466 | 467 | -------------------------------------------------------------------------------- /pypress/translations/zh/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Chinese translations for PROJECT. 2 | # Copyright (C) 2011 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2011. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2011-03-09 16:51+0800\n" 11 | "PO-Revision-Date: 2011-03-12 15:00+0800\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: zh \n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1)\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.5\n" 19 | 20 | #: pypress/__init__.py:193 21 | msgid "Login required" 22 | msgstr "请先登录" 23 | 24 | #: pypress/__init__.py:194 25 | msgid "Please login to see this page" 26 | msgstr "请先登录再查看此页" 27 | 28 | #: pypress/__init__.py:200 29 | msgid "Sorry, page not allowed" 30 | msgstr "对不起, 没有访问权限" 31 | 32 | #: pypress/__init__.py:206 33 | msgid "Sorry, page not found" 34 | msgstr "对不起, 页面没找到" 35 | 36 | #: pypress/__init__.py:212 37 | msgid "Sorry, an error has occurred" 38 | msgstr "对不起, 发生一个错误" 39 | 40 | #: pypress/helpers.py:107 41 | msgid "just now" 42 | msgstr "刚才" 43 | 44 | #: pypress/helpers.py:121 45 | #, python-format 46 | msgid "%(num)s year" 47 | msgid_plural "%(num)s years" 48 | msgstr[0] "%(num)s年" 49 | msgstr[1] "%(num)s年" 50 | 51 | #: pypress/helpers.py:122 52 | #, python-format 53 | msgid "%(num)s month" 54 | msgid_plural "%(num)s months" 55 | msgstr[0] "%(num)s月" 56 | msgstr[1] "%(num)s月" 57 | 58 | #: pypress/helpers.py:123 59 | #, python-format 60 | msgid "%(num)s week" 61 | msgid_plural "%(num)s weeks" 62 | msgstr[0] "%(num)s周" 63 | msgstr[1] "%(num)s周" 64 | 65 | #: pypress/helpers.py:124 66 | #, python-format 67 | msgid "%(num)s day" 68 | msgid_plural "%(num)s days" 69 | msgstr[0] "%(num)s天" 70 | msgstr[1] "%(num)s天" 71 | 72 | #: pypress/helpers.py:125 73 | #, python-format 74 | msgid "%(num)s hour" 75 | msgid_plural "%(num)s hours" 76 | msgstr[0] "%(num)s小时" 77 | msgstr[1] "%(num)s小时" 78 | 79 | #: pypress/helpers.py:126 80 | #, python-format 81 | msgid "%(num)s minute" 82 | msgid_plural "%(num)s minutes" 83 | msgstr[0] "%(num)s分钟" 84 | msgstr[1] "%(num)s分钟" 85 | 86 | #: pypress/helpers.py:127 87 | #, python-format 88 | msgid "%(num)s second" 89 | msgid_plural "%(num)s seconds" 90 | msgstr[0] "%(num)s秒" 91 | msgstr[1] "%(num)s秒" 92 | 93 | #: pypress/helpers.py:132 94 | #, python-format 95 | msgid "%(period)s ago" 96 | msgstr "%(period)s前" 97 | 98 | #: pypress/forms/account.py:23 99 | msgid "Username or email address" 100 | msgstr "用户名或邮箱" 101 | 102 | #: pypress/forms/account.py:25 103 | msgid "You must provide an email or username" 104 | msgstr "必须填写一个用户名或邮箱" 105 | 106 | #: pypress/forms/account.py:27 pypress/forms/account.py:45 107 | #: pypress/forms/account.py:83 108 | msgid "Password" 109 | msgstr "密码" 110 | 111 | #: pypress/forms/account.py:29 112 | msgid "Remember me" 113 | msgstr "记住我" 114 | 115 | #: pypress/forms/account.py:33 pypress/themes/default/templates/layout.html:48 116 | msgid "Login" 117 | msgstr "登录" 118 | 119 | #: pypress/forms/account.py:38 120 | msgid "Username" 121 | msgstr "用户名" 122 | 123 | #: pypress/forms/account.py:39 124 | msgid "Username required" 125 | msgstr "用户名必填" 126 | 127 | #: pypress/forms/account.py:42 pypress/forms/blog.py:55 128 | msgid "Nickname" 129 | msgstr "昵称" 130 | 131 | #: pypress/forms/account.py:43 pypress/forms/blog.py:56 132 | msgid "Nickname required" 133 | msgstr "昵称必填" 134 | 135 | #: pypress/forms/account.py:46 136 | msgid "Password required" 137 | msgstr "密码必填" 138 | 139 | #: pypress/forms/account.py:48 pypress/forms/account.py:89 140 | msgid "Password again" 141 | msgstr "重复密码" 142 | 143 | #: pypress/forms/account.py:50 pypress/forms/account.py:91 144 | msgid "Passwords don't match" 145 | msgstr "密码不一致" 146 | 147 | #: pypress/forms/account.py:52 148 | msgid "Email address" 149 | msgstr "邮箱" 150 | 151 | #: pypress/forms/account.py:53 152 | msgid "Email address required" 153 | msgstr "邮箱必填" 154 | 155 | #: pypress/forms/account.py:54 pypress/forms/account.py:76 156 | #: pypress/forms/blog.py:53 157 | msgid "A valid email address is required" 158 | msgstr "请填写一个正确的邮箱" 159 | 160 | #: pypress/forms/account.py:56 161 | msgid "Signup Code" 162 | msgstr "注册码" 163 | 164 | #: pypress/forms/account.py:60 pypress/templates/errors/403.html:15 165 | msgid "Signup" 166 | msgstr "注册" 167 | 168 | #: pypress/forms/account.py:65 169 | msgid "This username is taken" 170 | msgstr "用户名已存在" 171 | 172 | #: pypress/forms/account.py:70 173 | msgid "This email is taken" 174 | msgstr "邮箱已存在" 175 | 176 | #: pypress/forms/account.py:78 177 | msgid "Find password" 178 | msgstr "找回密码" 179 | 180 | #: pypress/forms/account.py:84 181 | msgid "Password is required" 182 | msgstr "密码必填" 183 | 184 | #: pypress/forms/account.py:86 185 | msgid "New Password" 186 | msgstr "新密码" 187 | 188 | #: pypress/forms/account.py:87 189 | msgid "New Password is required" 190 | msgstr "新密码必填" 191 | 192 | #: pypress/forms/account.py:93 pypress/forms/blog.py:31 193 | #: pypress/forms/blog.py:86 pypress/forms/blog.py:94 194 | msgid "Save" 195 | msgstr "保存" 196 | 197 | #: pypress/forms/account.py:98 198 | msgid "Recaptcha" 199 | msgstr "验证码" 200 | 201 | #: pypress/forms/account.py:100 202 | msgid "Delete" 203 | msgstr "删除" 204 | 205 | #: pypress/forms/account.py:105 pypress/forms/blog.py:26 206 | msgid "Content" 207 | msgstr "内容" 208 | 209 | #: pypress/forms/account.py:106 210 | msgid "Content is required" 211 | msgstr "评论必填" 212 | 213 | #: pypress/forms/account.py:108 214 | msgid "Send" 215 | msgstr "发送" 216 | 217 | #: pypress/forms/blog.py:21 218 | msgid "Title" 219 | msgstr "标题" 220 | 221 | #: pypress/forms/blog.py:22 222 | msgid "Title required" 223 | msgstr "标题必填" 224 | 225 | #: pypress/forms/blog.py:24 226 | msgid "Slug" 227 | msgstr "" 228 | 229 | #: pypress/forms/blog.py:28 pypress/templates/blog/_tags.html:2 230 | #: pypress/templates/blog/tags.html:7 231 | #: pypress/themes/default/templates/layout.html:30 232 | msgid "Tags" 233 | msgstr "标签" 234 | 235 | #: pypress/forms/blog.py:29 236 | msgid "Tags required" 237 | msgstr "标签必填" 238 | 239 | #: pypress/forms/blog.py:39 240 | msgid "Slug must be less than 50 characters" 241 | msgstr "Slug必须小于50个字符" 242 | 243 | #: pypress/forms/blog.py:45 244 | msgid "This slug is taken" 245 | msgstr "slug已存在" 246 | 247 | #: pypress/forms/blog.py:45 248 | msgid "Slug is required" 249 | msgstr "slug必填" 250 | 251 | #: pypress/forms/blog.py:51 pypress/forms/blog.py:77 252 | msgid "Email" 253 | msgstr "邮箱" 254 | 255 | #: pypress/forms/blog.py:52 256 | msgid "Email required" 257 | msgstr "邮箱必填" 258 | 259 | #: pypress/forms/blog.py:58 260 | msgid "Website" 261 | msgstr "网站" 262 | 263 | #: pypress/forms/blog.py:60 pypress/forms/blog.py:75 pypress/forms/blog.py:82 264 | msgid "A valid url is required" 265 | msgstr "请填写一个正确的url地址" 266 | 267 | #: pypress/forms/blog.py:62 268 | msgid "Comment" 269 | msgstr "评论" 270 | 271 | #: pypress/forms/blog.py:63 272 | msgid "Comment required" 273 | msgstr "评论必填" 274 | 275 | #: pypress/forms/blog.py:65 276 | msgid "Add comment" 277 | msgstr "添加评论" 278 | 279 | #: pypress/forms/blog.py:66 pypress/forms/blog.py:95 280 | msgid "Cancel" 281 | msgstr "取消" 282 | 283 | #: pypress/forms/blog.py:71 284 | msgid "Site name" 285 | msgstr "网站名称" 286 | 287 | #: pypress/forms/blog.py:72 288 | msgid "Name required" 289 | msgstr "名称必填" 290 | 291 | #: pypress/forms/blog.py:74 292 | msgid "link" 293 | msgstr "链接" 294 | 295 | #: pypress/forms/blog.py:78 296 | msgid "A valid email is required" 297 | msgstr "请填写一个正确的邮箱" 298 | 299 | #: pypress/forms/blog.py:80 300 | msgid "Logo" 301 | msgstr "" 302 | 303 | #: pypress/forms/blog.py:84 304 | msgid "Description" 305 | msgstr "介绍" 306 | 307 | #: pypress/forms/blog.py:91 308 | msgid "HTML" 309 | msgstr "" 310 | 311 | #: pypress/forms/blog.py:92 312 | msgid "HTML required" 313 | msgstr "html必填" 314 | 315 | #: pypress/forms/validators.py:17 316 | msgid "You can only use letters, numbers or dashes" 317 | msgstr "只允许使用字符,数字和下划线" 318 | 319 | #: pypress/models/blog.py:205 320 | msgid "Read more..." 321 | msgstr "阅读全部..." 322 | 323 | #: pypress/templates/account/login.html:5 pypress/templates/errors/403.html:14 324 | msgid "Login to pypress" 325 | msgstr "登录到pypress" 326 | 327 | #: pypress/templates/account/login.html:31 328 | msgid "Not a member yet ? Sign up !" 329 | msgstr "没有帐户? 立即注册" 330 | 331 | #: pypress/templates/account/signup.html:6 332 | msgid "Sign up to pypress" 333 | msgstr "注册pypress帐户" 334 | 335 | #: pypress/templates/blog/_archive.html:2 pypress/templates/blog/archive.html:7 336 | #: pypress/themes/default/templates/layout.html:29 337 | msgid "Archive" 338 | msgstr "存档" 339 | 340 | #: pypress/templates/blog/_links.html:2 pypress/templates/blog/links.html:7 341 | msgid "Links" 342 | msgstr "友情链接" 343 | 344 | #: pypress/templates/blog/_links.html:11 pypress/templates/blog/add_link.html:7 345 | msgid "Add link" 346 | msgstr "添加链接" 347 | 348 | #: pypress/templates/blog/_links.html:12 349 | msgid "More" 350 | msgstr "更多" 351 | 352 | #: pypress/templates/blog/_postnow.html:2 353 | msgid "Post Now" 354 | msgstr "写文章" 355 | 356 | #: pypress/templates/blog/_search.html:4 357 | msgid "Search all posts" 358 | msgstr "搜索文章" 359 | 360 | #: pypress/templates/blog/_search.html:5 pypress/templates/errors/404.html:14 361 | msgid "Search" 362 | msgstr "搜索" 363 | 364 | #: pypress/templates/blog/about.html:5 365 | #: pypress/themes/default/templates/layout.html:31 366 | msgid "About" 367 | msgstr "关于" 368 | 369 | #: pypress/templates/blog/links.html:15 370 | msgid "pass" 371 | msgstr "通过" 372 | 373 | #: pypress/templates/blog/links.html:18 pypress/templates/macros/_post.html:32 374 | msgid "delete" 375 | msgstr "删除" 376 | 377 | #: pypress/templates/blog/links.html:20 378 | msgid "Are you sure you want to delete this link ?" 379 | msgstr "确定要删除这个链接吗?" 380 | 381 | #: pypress/templates/blog/links.html:21 pypress/templates/blog/view.html:30 382 | #: pypress/templates/macros/_post.html:35 383 | msgid "yes" 384 | msgstr "是" 385 | 386 | #: pypress/templates/blog/links.html:22 pypress/templates/blog/view.html:31 387 | #: pypress/templates/macros/_post.html:36 388 | msgid "no" 389 | msgstr "否" 390 | 391 | #: pypress/templates/blog/list.html:23 392 | msgid "Modified at" 393 | msgstr "" 394 | 395 | #: pypress/templates/blog/list.html:30 396 | msgid "Nobody's posted anything yet." 397 | msgstr "还没有人发表过文章" 398 | 399 | #: pypress/templates/blog/people.html:12 400 | msgid "Joined in" 401 | msgstr "" 402 | 403 | #: pypress/templates/blog/people.html:28 404 | msgid "Posts" 405 | msgstr "文章" 406 | 407 | #: pypress/templates/blog/people.html:29 408 | msgid "Tweets" 409 | msgstr "" 410 | 411 | #: pypress/templates/blog/people.html:39 412 | msgid "Posted at" 413 | msgstr "" 414 | 415 | #: pypress/templates/blog/people.html:47 416 | msgid "You has not posted anything yet." 417 | msgstr "您还没有发表过文章" 418 | 419 | #: pypress/templates/blog/people.html:47 420 | msgid "Submit" 421 | msgstr "提交" 422 | 423 | #: pypress/templates/blog/people.html:49 424 | #, python-format 425 | msgid "%(name)s has not posted anything yet." 426 | msgstr "%(name)s 还没有发表过文章" 427 | 428 | #: pypress/templates/blog/search_result.html:8 429 | msgid "Search results for" 430 | msgstr "搜索结果:" 431 | 432 | #: pypress/templates/blog/search_result.html:20 433 | msgid "No posts found. Try a different search." 434 | msgstr "没有找到文章, 请尝试不同的搜索条件" 435 | 436 | #: pypress/templates/blog/submit.html:7 437 | msgid "Submit a post" 438 | msgstr "发布文章" 439 | 440 | #: pypress/templates/blog/submit.html:86 441 | msgid "Normal" 442 | msgstr "普通" 443 | 444 | #: pypress/templates/blog/submit.html:95 445 | msgid "Large" 446 | msgstr "较大" 447 | 448 | #: pypress/templates/blog/submit.html:104 449 | msgid "Maximum" 450 | msgstr "极大" 451 | 452 | #: pypress/templates/blog/template_edit.html:8 453 | msgid "Edit template" 454 | msgstr "编辑模板" 455 | 456 | #: pypress/templates/blog/view.html:21 457 | msgid "Modified at " 458 | msgstr "" 459 | 460 | #: pypress/templates/blog/view.html:24 461 | msgid "edit this post" 462 | msgstr "编辑文章" 463 | 464 | #: pypress/templates/blog/view.html:27 465 | msgid "delete this post" 466 | msgstr "删除文章" 467 | 468 | #: pypress/templates/blog/view.html:29 469 | msgid "Are you sure you want to delete this post ?" 470 | msgstr "确定要删除这个文章吗?" 471 | 472 | #: pypress/templates/blog/view.html:46 473 | msgid "Add a comment" 474 | msgstr "添加评论" 475 | 476 | #: pypress/templates/blog/view.html:64 477 | msgid "Comments" 478 | msgstr "评论" 479 | 480 | #: pypress/templates/blog/view.html:72 481 | msgid "No comments have been posted yet." 482 | msgstr "还没有评论" 483 | 484 | #: pypress/templates/errors/403.html:5 485 | msgid "403, Page Not Allowed" 486 | msgstr "对不起, 没有访问权限" 487 | 488 | #: pypress/templates/errors/403.html:12 489 | msgid "Maybe we can help you out though:" 490 | msgstr "也许我们能帮您找到其他途径:" 491 | 492 | #: pypress/templates/errors/403.html:15 493 | msgid "Not a member yet?" 494 | msgstr "没有帐户? " 495 | 496 | #: pypress/templates/errors/404.html:5 497 | msgid "404, Page Not Found" 498 | msgstr "对不起, 页面没找到" 499 | 500 | #: pypress/templates/errors/404.html:12 501 | msgid "Maybe you can try search:" 502 | msgstr "也许您应该尝试下搜索:" 503 | 504 | #: pypress/templates/errors/500.html:5 505 | msgid "500, An Error Has Occurred" 506 | msgstr "对不起, 发生一个错误" 507 | 508 | #: pypress/templates/macros/_page.html:7 509 | msgid "newer" 510 | msgstr "更新" 511 | 512 | #: pypress/templates/macros/_page.html:13 513 | msgid "older" 514 | msgstr "更旧" 515 | 516 | #: pypress/templates/macros/_post.html:9 517 | msgid "permalink" 518 | msgstr "" 519 | 520 | #: pypress/templates/macros/_post.html:18 521 | msgid "reply" 522 | msgstr "回复" 523 | 524 | #: pypress/templates/macros/_post.html:34 525 | msgid "Are you sure you want to delete this comment ?" 526 | msgstr "确定要删除这个评论吗?" 527 | 528 | #: pypress/themes/default/templates/layout.html:28 529 | msgid "Home" 530 | msgstr "首页" 531 | 532 | #: pypress/themes/default/templates/layout.html:48 533 | msgid "Logout" 534 | msgstr "登出" 535 | 536 | #: pypress/views/account.py:59 537 | #, python-format 538 | msgid "Welcome back, %(name)s" 539 | msgstr "欢迎回来, %(name)s" 540 | 541 | #: pypress/views/account.py:70 542 | msgid "Sorry, invalid login" 543 | msgstr "对不起, 登录失败" 544 | 545 | #: pypress/views/account.py:95 546 | #, python-format 547 | msgid "Welcome, %(name)s" 548 | msgstr "欢迎, %(name)s" 549 | 550 | #: pypress/views/account.py:104 551 | msgid "Code is not allowed" 552 | msgstr "注册码不正确" 553 | 554 | #: pypress/views/account.py:112 555 | msgid "You are now logged out" 556 | msgstr "已成功登出" 557 | 558 | #: pypress/views/account.py:129 559 | msgid "You twitter's access token is already exists" 560 | msgstr "你的twitter访问权限已存在" 561 | 562 | #: pypress/views/account.py:178 563 | msgid "Twitter request success" 564 | msgstr "Twitter 认证成功" 565 | 566 | #: pypress/views/frontend.py:130 567 | msgid "Twitter posting is success" 568 | msgstr "Twitter 发推成功" 569 | 570 | #: pypress/views/frontend.py:132 571 | msgid "Twitter posting is failed" 572 | msgstr "Twitter 发推失败" 573 | 574 | #: pypress/views/frontend.py:156 575 | msgid "Please select a picture" 576 | msgstr "请选择一个图片" 577 | 578 | #: pypress/views/frontend.py:208 579 | msgid "Template file does not exists" 580 | msgstr "模板文件不存在" 581 | 582 | #: pypress/views/frontend.py:218 583 | msgid "Saving success" 584 | msgstr "保存成功" 585 | 586 | #: pypress/views/link.py:57 587 | msgid "Adding success" 588 | msgstr "添加成功" 589 | 590 | #: pypress/views/post.py:44 591 | msgid "Posting success" 592 | msgstr "发布成功" 593 | 594 | #: pypress/views/post.py:77 595 | msgid "Post has been changed" 596 | msgstr "文章已修改" 597 | 598 | #: pypress/views/post.py:106 599 | msgid "The post has been deleted" 600 | msgstr "文章已删除" 601 | 602 | #: pypress/views/post.py:137 603 | msgid "Thanks for your comment" 604 | msgstr "谢谢您的评论" 605 | 606 | --------------------------------------------------------------------------------