Anonymous avatar Anonymous committed 416385c

make things right

Comments (0)

Files changed (42)

-# UWSGI configuration
-
-import uwsgi
-import production_settings
-
-from newsmeme import create_app
-
-application = create_app(production_settings)
-uwsgi.applications = {"/":application}
-
+# UWSGI configuration
+
+import uwsgi
+import production_settings
+
+from newsmeme import create_app
+
+application = create_app(production_settings)
+uwsgi.applications = {"/":application}
+
-import sys
-
-from fabric.api import env, run, cd
-
-"""
-Sample fabric file. Assumes the following on production server:
-
-- virtualenv + virtualenvwrapper
-- mercurial
-- supervisord (running gunicorn/uwsgi etc.)
-
-Copy and modify as required for your own particular setup.
-
-Keep your private settings in a separate module, fab_settings.py, in
-the same directory as this file.
-"""
-
-try:
-    import fab_settings as settings
-except ImportError:
-    print "You must provide a valid fab_settings.py module in this directory"
-    sys.exit(1)
-
-
-def server():
-    env.hosts = settings.SERVER_HOSTS
-    env.user = settings.SERVER_USER
-    env.password = settings.SERVER_PASSWORD
-
-
-def deploy():
-    """
-    Pulls latest code into staging, runs tests, then pulls into live.
-    """
-    providers = (server,)
-    with cd(settings.STAGING_DIR):
-        run("hg pull -u")
-        run("workon %s;nosetests" % settings.VIRTUALENV)
-    with cd(settings.PRODUCTION_DIR):
-        run("hg pull -u")
-
-
-def reload():
-    """
-    Deploys and then reloads application server.
-    """
-    deploy()
-    run("supervisorctl -c %s restart %s" % (settings.SUPERVISOR_CONF,
-                                            settings.SUPERVISOR_CMD))
-
-
-def upgrade():
-    """
-    Updates all required packages, runs tests, then updates packages on
-    live, then restarts server.
-    """
-
-    with cd(settings.PRODUCTION_DIR):
-        run("workon %s; python setup.py develop -U" % settings.VIRTUALENV)
-
-    reload()
-
-
+import sys
+
+from fabric.api import env, run, cd
+
+"""
+Sample fabric file. Assumes the following on production server:
+
+- virtualenv + virtualenvwrapper
+- mercurial
+- supervisord (running gunicorn/uwsgi etc.)
+
+Copy and modify as required for your own particular setup.
+
+Keep your private settings in a separate module, fab_settings.py, in
+the same directory as this file.
+"""
+
+try:
+    import fab_settings as settings
+except ImportError:
+    print "You must provide a valid fab_settings.py module in this directory"
+    sys.exit(1)
+
+
+def server():
+    env.hosts = settings.SERVER_HOSTS
+    env.user = settings.SERVER_USER
+    env.password = settings.SERVER_PASSWORD
+
+
+def deploy():
+    """
+    Pulls latest code into staging, runs tests, then pulls into live.
+    """
+    providers = (server,)
+    with cd(settings.STAGING_DIR):
+        run("hg pull -u")
+        run("workon %s;nosetests" % settings.VIRTUALENV)
+    with cd(settings.PRODUCTION_DIR):
+        run("hg pull -u")
+
+
+def reload():
+    """
+    Deploys and then reloads application server.
+    """
+    deploy()
+    run("supervisorctl -c %s restart %s" % (settings.SUPERVISOR_CONF,
+                                            settings.SUPERVISOR_CMD))
+
+
+def upgrade():
+    """
+    Updates all required packages, runs tests, then updates packages on
+    live, then restarts server.
+    """
+
+    with cd(settings.PRODUCTION_DIR):
+        run("workon %s; python setup.py develop -U" % settings.VIRTUALENV)
+
+    reload()
+
+

gunicorn-deploy.py

-# Production WSGI configuration
-
-import production_settings
-
-from newsmeme import create_app
-
-app = create_app(production_settings)
-
+# Production WSGI configuration
+
+import production_settings
+
+from newsmeme import create_app
+
+app = create_app(production_settings)
+
-# -*- coding: utf-8 -*-
-"""
-    manage.py
-    ~~~~~~~~~
-
-    Description of the module goes here...
-
-    :copyright: (c) 2010 by Dan Jacob.
-    :license: BSD, see LICENSE for more details.
-"""
-import sys
-import feedparser
-
-from flask import current_app
-
-from flaskext.script import Manager, prompt, prompt_pass, \
-    prompt_bool, prompt_choices
-
-from flaskext.mail import Message
-
-from newsmeme import create_app
-from newsmeme.extensions import db, mail
-from newsmeme.models import Post, User, Comment, Tag
-
-manager = Manager(create_app)
-
-@manager.command
-def rungevent(port=5000, name=''):
-    from gevent.wsgi import WSGIServer
-    server = WSGIServer((name, port), 
-                        current_app._get_current_object())
-    server.serve_forever()
-
-
-@manager.option("-u", "--url", dest="url", help="Feed URL")
-@manager.option("-n", "--username", dest="username", help="Save to user")
-def importfeed(url, username):
-    """
-    Bulk import news from a feed. For testing only !
-    """
-
-    user = User.query.filter_by(username=username).first()
-    if not user:
-        print "User %s does not exist" % username
-        sys.exit(1)
-    d = feedparser.parse(url)
-    for entry in d['entries']:
-        post = Post(author=user,
-                    title=entry.title[:200],
-                    link=entry.link)
-
-        db.session.add(post)
-    db.session.commit()
-
-@manager.option('-u', '--username', dest="username", required=False)
-@manager.option('-p', '--password', dest="password", required=False)
-@manager.option('-e', '--email', dest="email", required=False)
-@manager.option('-r', '--role', dest="role", required=False)
-def createuser(username=None, password=None, email=None, role=None):
-    """
-    Create a new user
-    """
-    
-    if username is None:
-        while True:
-            username = prompt("Username")
-            user = User.query.filter(User.username==username).first()
-            if user is not None:
-                print "Username %s is already taken" % username
-            else:
-                break
-
-    if email is None:
-        while True:
-            email = prompt("Email address")
-            user = User.query.filter(User.email==email).first()
-            if user is not None:
-                print "Email %s is already taken" % email
-            else:
-                break
-
-    if password is None:
-        password = prompt_pass("Password")
-
-        while True:
-            password_again = prompt_pass("Password again")
-            if password != password_again:
-                print "Passwords do not match"
-            else:
-                break
-    
-    roles = (
-        (User.MEMBER, "member"),
-        (User.MODERATOR, "moderator"),
-        (User.ADMIN, "admin"),
-    )
-
-    if role is None:
-        role = prompt_choices("Role", roles, resolve=int, default=User.MEMBER)
-
-    user = User(username=username,
-                email=email,
-                password=password,
-                role=role)
-
-    db.session.add(user)
-    db.session.commit()
-
-    print "User created with ID", user.id
-
-
-@manager.command
-def createall():
-    "Creates database tables"
-    
-    db.create_all()
-
-@manager.command
-def dropall():
-    "Drops all database tables"
-    
-    if prompt_bool("Are you sure ? You will lose all your data !"):
-        db.drop_all()
-
-@manager.command
-def mailall():
-    "Sends an email to all users"
-    
-    subject = prompt("Subject")
-    message = prompt("Message")
-    from_address = prompt("From", default="support@thenewsmeme.com")
-    if prompt_bool("Are you sure ? Email will be sent to everyone!"):
-        with mail.connect() as conn:
-            for user in User.query:
-                message = Message(subject=subject,
-                                  body=message,
-                                  sender=from_address,
-                                  recipients=[user.email])
-
-                conn.send(message)
-
-
-@manager.shell
-def make_shell_context():
-    return dict(app=current_app, 
-                db=db,
-                Post=Post,
-                User=User,
-                Tag=Tag,
-                Comment=Comment)
-
-
-manager.add_option('-c', '--config',
-                   dest="config",
-                   required=False,
-                   help="config file")
-
-if __name__ == "__main__":
-    manager.run()
+# -*- coding: utf-8 -*-
+"""
+    manage.py
+    ~~~~~~~~~
+
+    Description of the module goes here...
+
+    :copyright: (c) 2010 by Dan Jacob.
+    :license: BSD, see LICENSE for more details.
+"""
+import sys
+import feedparser
+
+from flask import current_app
+
+from flask.ext.script import Manager, prompt, prompt_pass, \
+    prompt_bool, prompt_choices
+
+from flask.ext.mail import Message
+
+from newsmeme import create_app
+from newsmeme.extensions import db, mail
+from newsmeme.models import Post, User, Comment, Tag
+
+manager = Manager(create_app)
+
+@manager.command
+def rungevent(port=5000, name=''):
+    from gevent.wsgi import WSGIServer
+    server = WSGIServer((name, port), 
+                        current_app._get_current_object())
+    server.serve_forever()
+
+
+@manager.option("-u", "--url", dest="url", help="Feed URL")
+@manager.option("-n", "--username", dest="username", help="Save to user")
+def importfeed(url, username):
+    """
+    Bulk import news from a feed. For testing only !
+    """
+
+    user = User.query.filter_by(username=username).first()
+    if not user:
+        print "User %s does not exist" % username
+        sys.exit(1)
+    d = feedparser.parse(url)
+    for entry in d['entries']:
+        post = Post(author=user,
+                    title=entry.title[:200],
+                    link=entry.link)
+
+        db.session.add(post)
+    db.session.commit()
+
+@manager.option('-u', '--username', dest="username", required=False)
+@manager.option('-p', '--password', dest="password", required=False)
+@manager.option('-e', '--email', dest="email", required=False)
+@manager.option('-r', '--role', dest="role", required=False)
+def createuser(username=None, password=None, email=None, role=None):
+    """
+    Create a new user
+    """
+    
+    if username is None:
+        while True:
+            username = prompt("Username")
+            user = User.query.filter(User.username==username).first()
+            if user is not None:
+                print "Username %s is already taken" % username
+            else:
+                break
+
+    if email is None:
+        while True:
+            email = prompt("Email address")
+            user = User.query.filter(User.email==email).first()
+            if user is not None:
+                print "Email %s is already taken" % email
+            else:
+                break
+
+    if password is None:
+        password = prompt_pass("Password")
+
+        while True:
+            password_again = prompt_pass("Password again")
+            if password != password_again:
+                print "Passwords do not match"
+            else:
+                break
+    
+    roles = (
+        (User.MEMBER, "member"),
+        (User.MODERATOR, "moderator"),
+        (User.ADMIN, "admin"),
+    )
+
+    if role is None:
+        role = prompt_choices("Role", roles, resolve=int, default=User.MEMBER)
+
+    user = User(username=username,
+                email=email,
+                password=password,
+                role=role)
+
+    db.session.add(user)
+    db.session.commit()
+
+    print "User created with ID", user.id
+
+
+@manager.command
+def createall():
+    "Creates database tables"
+    
+    db.create_all()
+
+@manager.command
+def dropall():
+    "Drops all database tables"
+    
+    if prompt_bool("Are you sure ? You will lose all your data !"):
+        db.drop_all()
+
+@manager.command
+def mailall():
+    "Sends an email to all users"
+    
+    subject = prompt("Subject")
+    message = prompt("Message")
+    from_address = prompt("From", default="support@thenewsmeme.com")
+    if prompt_bool("Are you sure ? Email will be sent to everyone!"):
+        with mail.connect() as conn:
+            for user in User.query:
+                message = Message(subject=subject,
+                                  body=message,
+                                  sender=from_address,
+                                  recipients=[user.email])
+
+                conn.send(message)
+
+
+@manager.shell
+def make_shell_context():
+    return dict(app=current_app, 
+                db=db,
+                Post=Post,
+                User=User,
+                Tag=Tag,
+                Comment=Comment)
+
+
+manager.add_option('-c', '--config',
+                   dest="config",
+                   required=False,
+                   help="config file")
+
+if __name__ == "__main__":
+    manager.run()

newsmeme/__init__.py

-# -*- coding: utf-8 -*-
-"""
-    __init__.py
-    ~~~~~~~~~~~
-
-    :copyright: (c) 2010 by Dan Jacob.
-    :license: BSD, see LICENSE for more details.
-"""
-
-from newsmeme.application import create_app
-
+# -*- coding: utf-8 -*-
+"""
+    __init__.py
+    ~~~~~~~~~~~
+
+    :copyright: (c) 2010 by Dan Jacob.
+    :license: BSD, see LICENSE for more details.
+"""
+
+from newsmeme.application import create_app
+

newsmeme/application.py

-# -*- coding: utf-8 -*-
-"""
-    application.py
-    ~~~~~~~~~~~
-
-    Application configuration
-
-    :copyright: (c) 2010 by Dan Jacob.
-    :license: BSD, see LICENSE for more details.
-"""
-import os
-import logging
-
-from logging.handlers import SMTPHandler, RotatingFileHandler
-
-from flask import Flask, Response, request, g, \
-        jsonify, redirect, url_for, flash
-
-from flaskext.babel import Babel, gettext as _
-from flaskext.themes import setup_themes
-from flaskext.principal import Principal, identity_loaded
-
-from newsmeme import helpers
-from newsmeme import views
-from newsmeme.config import DefaultConfig
-from newsmeme.models import User, Tag
-from newsmeme.helpers import render_template
-from newsmeme.extensions import db, mail, oid, cache
-
-__all__ = ["create_app"]
-
-DEFAULT_APP_NAME = "newsmeme"
-
-DEFAULT_MODULES = (
-    (views.frontend, ""),
-    (views.post, "/post"),
-    (views.user, "/user"),
-    (views.comment, "/comment"),
-    (views.account, "/acct"),
-    (views.feeds, "/feeds"),
-    (views.openid, "/openid"),
-    (views.api, "/api"),
-)
-
-def create_app(config=None, app_name=None, modules=None):
-
-    if app_name is None:
-        app_name = DEFAULT_APP_NAME
-
-    if modules is None:
-        modules = DEFAULT_MODULES
-
-    app = Flask(app_name)
-
-    configure_app(app, config)
-
-    configure_logging(app)
-    configure_errorhandlers(app)
-    configure_extensions(app)
-    configure_before_handlers(app)
-    configure_template_filters(app)
-    configure_context_processors(app)
-    # configure_after_handlers(app)
-    configure_modules(app, modules)
-
-    return app
-
-
-def configure_app(app, config):
-    
-    app.config.from_object(DefaultConfig())
-
-    if config is not None:
-        app.config.from_object(config)
-
-    app.config.from_envvar('APP_CONFIG', silent=True)
-
-
-def configure_modules(app, modules):
-    
-    for module, url_prefix in modules:
-        app.register_module(module, url_prefix=url_prefix)
-
-
-def configure_template_filters(app):
-
-    @app.template_filter()
-    def timesince(value):
-        return helpers.timesince(value)
-
-
-def configure_before_handlers(app):
-
-    @app.before_request
-    def authenticate():
-        g.user = getattr(g.identity, 'user', None)
-
-
-def configure_context_processors(app):
-
-    @app.context_processor
-    def get_tags():
-        tags = cache.get("tags")
-        if tags is None:
-            tags = Tag.query.order_by(Tag.num_posts.desc()).limit(10).all()
-            cache.set("tags", tags)
-
-        return dict(tags=tags)
-
-    @app.context_processor
-    def config():
-        return dict(config=app.config)
-
-
-def configure_extensions(app):
-
-    mail.init_app(app)
-    db.init_app(app)
-    oid.init_app(app)
-    cache.init_app(app)
-
-    setup_themes(app)
-
-    # more complicated setups
-
-    configure_identity(app)
-    configure_i18n(app)
-    
-
-def configure_identity(app):
-
-    Principal(app)
-
-    @identity_loaded.connect_via(app)
-    def on_identity_loaded(sender, identity):
-        g.user = User.query.from_identity(identity)
-
-
-def configure_i18n(app):
-
-    babel = Babel(app)
-
-    @babel.localeselector
-    def get_locale():
-        accept_languages = app.config.get('ACCEPT_LANGUAGES', 
-                                               ['en_gb'])
-
-        return request.accept_languages.best_match(accept_languages)
-
-
-def configure_errorhandlers(app):
-
-    if app.testing:
-        return
-
-    @app.errorhandler(404)
-    def page_not_found(error):
-        if request.is_xhr:
-            return jsonify(error=_('Sorry, page not found'))
-        return render_template("errors/404.html", error=error)
-
-    @app.errorhandler(403)
-    def forbidden(error):
-        if request.is_xhr:
-            return jsonify(error=_('Sorry, not allowed'))
-        return render_template("errors/403.html", error=error)
-
-    @app.errorhandler(500)
-    def server_error(error):
-        if request.is_xhr:
-            return jsonify(error=_('Sorry, an error has occurred'))
-        return render_template("errors/500.html", error=error)
-
-    @app.errorhandler(401)
-    def unauthorized(error):
-        if request.is_xhr:
-            return jsonfiy(error=_("Login required"))
-        flash(_("Please login to see this page"), "error")
-        return redirect(url_for("account.login", next=request.path))
-
-
-def configure_logging(app):
-    if app.debug or app.testing:
-        return
-
-    mail_handler = \
-        SMTPHandler(app.config['MAIL_SERVER'],
-                    'error@newsmeme.com',
-                    app.config['ADMINS'], 
-                    'application error',
-                    (
-                        app.config['MAIL_USERNAME'],
-                        app.config['MAIL_PASSWORD'],
-                    ))
-
-    mail_handler.setLevel(logging.ERROR)
-    app.logger.addHandler(mail_handler)
-
-    formatter = logging.Formatter(
-        '%(asctime)s %(levelname)s: %(message)s '
-        '[in %(pathname)s:%(lineno)d]')
-
-    debug_log = os.path.join(app.root_path, 
-                             app.config['DEBUG_LOG'])
-
-    debug_file_handler = \
-        RotatingFileHandler(debug_log,
-                            maxBytes=100000,
-                            backupCount=10)
-
-    debug_file_handler.setLevel(logging.DEBUG)
-    debug_file_handler.setFormatter(formatter)
-    app.logger.addHandler(debug_file_handler)
-
-    error_log = os.path.join(app.root_path, 
-                             app.config['ERROR_LOG'])
-
-    error_file_handler = \
-        RotatingFileHandler(error_log,
-                            maxBytes=100000,
-                            backupCount=10)
-
-    error_file_handler.setLevel(logging.ERROR)
-    error_file_handler.setFormatter(formatter)
-    app.logger.addHandler(error_file_handler)
-
-
-
+# -*- coding: utf-8 -*-
+"""
+    application.py
+    ~~~~~~~~~~~
+
+    Application configuration
+
+    :copyright: (c) 2010 by Dan Jacob.
+    :license: BSD, see LICENSE for more details.
+"""
+import os
+import logging
+
+from logging.handlers import SMTPHandler, RotatingFileHandler
+
+from flask import Flask, Response, request, g, \
+        jsonify, redirect, url_for, flash
+
+from flask.ext.babel import Babel, gettext as _
+from flask.ext.themes import setup_themes
+from flask.ext.principal import Principal, identity_loaded
+
+from newsmeme import helpers
+from newsmeme import views
+from newsmeme.config import DefaultConfig
+from newsmeme.models import User, Tag
+from newsmeme.helpers import render_template
+from newsmeme.extensions import db, mail, oid, cache
+
+__all__ = ["create_app"]
+
+DEFAULT_APP_NAME = "newsmeme"
+
+DEFAULT_MODULES = (
+    (views.frontend, ""),
+    (views.post, "/post"),
+    (views.user, "/user"),
+    (views.comment, "/comment"),
+    (views.account, "/acct"),
+    (views.feeds, "/feeds"),
+    (views.openid, "/openid"),
+    (views.api, "/api"),
+)
+
+def create_app(config=None, app_name=None, modules=None):
+
+    if app_name is None:
+        app_name = DEFAULT_APP_NAME
+
+    if modules is None:
+        modules = DEFAULT_MODULES
+
+    app = Flask(app_name)
+
+    configure_app(app, config)
+
+    configure_logging(app)
+    configure_errorhandlers(app)
+    configure_extensions(app)
+    configure_before_handlers(app)
+    configure_template_filters(app)
+    configure_context_processors(app)
+    # configure_after_handlers(app)
+    configure_modules(app, modules)
+
+    return app
+
+
+def configure_app(app, config):
+    
+    app.config.from_object(DefaultConfig())
+
+    if config is not None:
+        app.config.from_object(config)
+
+    app.config.from_envvar('APP_CONFIG', silent=True)
+
+
+def configure_modules(app, modules):
+    
+    for module, url_prefix in modules:
+        app.register_module(module, url_prefix=url_prefix)
+
+
+def configure_template_filters(app):
+
+    @app.template_filter()
+    def timesince(value):
+        return helpers.timesince(value)
+
+
+def configure_before_handlers(app):
+
+    @app.before_request
+    def authenticate():
+        g.user = getattr(g.identity, 'user', None)
+
+
+def configure_context_processors(app):
+
+    @app.context_processor
+    def get_tags():
+        tags = cache.get("tags")
+        if tags is None:
+            tags = Tag.query.order_by(Tag.num_posts.desc()).limit(10).all()
+            cache.set("tags", tags)
+
+        return dict(tags=tags)
+
+    @app.context_processor
+    def config():
+        return dict(config=app.config)
+
+
+def configure_extensions(app):
+
+    mail.init_app(app)
+    db.init_app(app)
+    oid.init_app(app)
+    cache.init_app(app)
+
+    setup_themes(app)
+
+    # more complicated setups
+
+    configure_identity(app)
+    configure_i18n(app)
+    
+
+def configure_identity(app):
+
+    Principal(app)
+
+    @identity_loaded.connect_via(app)
+    def on_identity_loaded(sender, identity):
+        g.user = User.query.from_identity(identity)
+
+
+def configure_i18n(app):
+
+    babel = Babel(app)
+
+    @babel.localeselector
+    def get_locale():
+        accept_languages = app.config.get('ACCEPT_LANGUAGES', 
+                                               ['en_gb'])
+
+        return request.accept_languages.best_match(accept_languages)
+
+
+def configure_errorhandlers(app):
+
+    if app.testing:
+        return
+
+    @app.errorhandler(404)
+    def page_not_found(error):
+        if request.is_xhr:
+            return jsonify(error=_('Sorry, page not found'))
+        return render_template("errors/404.html", error=error)
+
+    @app.errorhandler(403)
+    def forbidden(error):
+        if request.is_xhr:
+            return jsonify(error=_('Sorry, not allowed'))
+        return render_template("errors/403.html", error=error)
+
+    @app.errorhandler(500)
+    def server_error(error):
+        if request.is_xhr:
+            return jsonify(error=_('Sorry, an error has occurred'))
+        return render_template("errors/500.html", error=error)
+
+    @app.errorhandler(401)
+    def unauthorized(error):
+        if request.is_xhr:
+            return jsonfiy(error=_("Login required"))
+        flash(_("Please login to see this page"), "error")
+        return redirect(url_for("account.login", next=request.path))
+
+
+def configure_logging(app):
+    if app.debug or app.testing:
+        return
+
+    mail_handler = \
+        SMTPHandler(app.config['MAIL_SERVER'],
+                    'error@newsmeme.com',
+                    app.config['ADMINS'], 
+                    'application error',
+                    (
+                        app.config['MAIL_USERNAME'],
+                        app.config['MAIL_PASSWORD'],
+                    ))
+
+    mail_handler.setLevel(logging.ERROR)
+    app.logger.addHandler(mail_handler)
+
+    formatter = logging.Formatter(
+        '%(asctime)s %(levelname)s: %(message)s '
+        '[in %(pathname)s:%(lineno)d]')
+
+    debug_log = os.path.join(app.root_path, 
+                             app.config['DEBUG_LOG'])
+
+    debug_file_handler = \
+        RotatingFileHandler(debug_log,
+                            maxBytes=100000,
+                            backupCount=10)
+
+    debug_file_handler.setLevel(logging.DEBUG)
+    debug_file_handler.setFormatter(formatter)
+    app.logger.addHandler(debug_file_handler)
+
+    error_log = os.path.join(app.root_path, 
+                             app.config['ERROR_LOG'])
+
+    error_file_handler = \
+        RotatingFileHandler(error_log,
+                            maxBytes=100000,
+                            backupCount=10)
+
+    error_file_handler.setLevel(logging.ERROR)
+    error_file_handler.setFormatter(formatter)
+    app.logger.addHandler(error_file_handler)
+
+
+

newsmeme/config.py

-# -*- coding: utf-8 -*-
-"""
-    config.py
-    ~~~~~~~~~~~
-
-    Default configuration
-
-    :copyright: (c) 2010 by Dan Jacob.
-    :license: BSD, see LICENSE for more details.
-"""
-
-from newsmeme import views
-
-class DefaultConfig(object):
-    """
-    Default configuration for a newsmeme application.
-    """
-
-    DEBUG = True
-
-    # change this in your production settings !!!
-
-    SECRET_KEY = "secret"
-
-    # keys for localhost. Change as appropriate.
-
-    RECAPTCHA_PUBLIC_KEY = '6LeYIbsSAAAAACRPIllxA7wvXjIE411PfdB2gt2J'
-    RECAPTCHA_PRIVATE_KEY = '6LeYIbsSAAAAAJezaIq3Ft_hSTo0YtyeFG-JgRtu'
-
-    SQLALCHEMY_DATABASE_URI = "sqlite:///newsmeme.db"
-
-    SQLALCHEMY_ECHO = False
-
-    MAIL_DEBUG = DEBUG
-
-    ADMINS = ()
-
-    DEFAULT_MAIL_SENDER = "support@thenewsmeme.com"
-
-    ACCEPT_LANGUAGES = ['en_gb', 'fi']
-
-    DEBUG_LOG = 'logs/debug.log'
-    ERROR_LOG = 'logs/error.log'
-
-    THEME = 'newsmeme'
-
-    CACHE_TYPE = "simple"
-    CACHE_DEFAULT_TIMEOUT = 300
-
-
-class TestConfig(object):
-
-    TESTING = True
-    CSRF_ENABLED = False
-    SQLALCHEMY_DATABASE_URI = "sqlite://"
-    SQLALCHEMY_ECHO = False
-
-
-
-
+# -*- coding: utf-8 -*-
+"""
+    config.py
+    ~~~~~~~~~~~
+
+    Default configuration
+
+    :copyright: (c) 2010 by Dan Jacob.
+    :license: BSD, see LICENSE for more details.
+"""
+
+from newsmeme import views
+
+class DefaultConfig(object):
+    """
+    Default configuration for a newsmeme application.
+    """
+
+    DEBUG = True
+
+    # change this in your production settings !!!
+
+    SECRET_KEY = "secret"
+
+    # keys for localhost. Change as appropriate.
+
+    RECAPTCHA_PUBLIC_KEY = '6LeYIbsSAAAAACRPIllxA7wvXjIE411PfdB2gt2J'
+    RECAPTCHA_PRIVATE_KEY = '6LeYIbsSAAAAAJezaIq3Ft_hSTo0YtyeFG-JgRtu'
+
+    SQLALCHEMY_DATABASE_URI = "sqlite:///newsmeme.db"
+
+    SQLALCHEMY_ECHO = False
+
+    MAIL_DEBUG = DEBUG
+
+    ADMINS = ()
+
+    DEFAULT_MAIL_SENDER = "support@thenewsmeme.com"
+
+    ACCEPT_LANGUAGES = ['en_gb', 'fi']
+
+    DEBUG_LOG = 'logs/debug.log'
+    ERROR_LOG = 'logs/error.log'
+
+    THEME = 'newsmeme'
+
+    CACHE_TYPE = "simple"
+    CACHE_DEFAULT_TIMEOUT = 300
+
+
+class TestConfig(object):
+
+    TESTING = True
+    CSRF_ENABLED = False
+    SQLALCHEMY_DATABASE_URI = "sqlite://"
+    SQLALCHEMY_ECHO = False
+
+
+
+

newsmeme/decorators.py

-import functools
-
-from flask import g
-
-def keep_login_url(func):
-    """
-    Adds attribute g.keep_login_url in order to pass the current
-    login URL to login/signup links.
-    """
-    @functools.wraps(func)
-    def wrapper(*args, **kwargs):
-        g.keep_login_url = True
-        return func(*args, **kwargs)
-    return wrapper
-
+import functools
+
+from flask import g
+
+def keep_login_url(func):
+    """
+    Adds attribute g.keep_login_url in order to pass the current
+    login URL to login/signup links.
+    """
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        g.keep_login_url = True
+        return func(*args, **kwargs)
+    return wrapper
+

newsmeme/extensions.py

-from flaskext.mail import Mail
-from flaskext.openid import OpenID
-from flaskext.sqlalchemy import SQLAlchemy
-from flaskext.cache import Cache
-
-__all__ = ['oid', 'mail', 'db']
-
-oid = OpenID()
-mail = Mail()
-db = SQLAlchemy()
-cache = Cache()
-
+from flask.ext.mail import Mail
+from flask.ext.openid import OpenID
+from flask.ext.sqlalchemy import SQLAlchemy
+from flask.ext.cache import Cache
+
+__all__ = ['oid', 'mail', 'db']
+
+oid = OpenID()
+mail = Mail()
+db = SQLAlchemy()
+cache = Cache()
+

newsmeme/forms/__init__.py

-# -*- coding: utf-8 -*-
-"""
-    forms.py
-    ~~~~~~~~
-
-    Description of the module goes here...
-
-    :copyright: (c) 2010 by Dan Jacob.
-    :license: BSD, see LICENSE for more details.
-"""
-
-from .account import LoginForm, SignupForm, EditAccountForm, \
-        RecoverPasswordForm, ChangePasswordForm, DeleteAccountForm
-
-from .openid import OpenIdLoginForm, OpenIdSignupForm
-from .post import PostForm
-from .contact import ContactForm, MessageForm
-from .comment import CommentForm, CommentAbuseForm
+# -*- coding: utf-8 -*-
+"""
+    forms.py
+    ~~~~~~~~
+
+    Description of the module goes here...
+
+    :copyright: (c) 2010 by Dan Jacob.
+    :license: BSD, see LICENSE for more details.
+"""
+
+from .account import LoginForm, SignupForm, EditAccountForm, \
+        RecoverPasswordForm, ChangePasswordForm, DeleteAccountForm
+
+from .openid import OpenIdLoginForm, OpenIdSignupForm
+from .post import PostForm
+from .contact import ContactForm, MessageForm
+from .comment import CommentForm, CommentAbuseForm

newsmeme/forms/account.py

-# -*- coding: utf-8 -*-
-from flaskext.wtf import Form, HiddenField, BooleanField, TextField, \
-        PasswordField, SubmitField, TextField, RecaptchaField, \
-        ValidationError, required, email, equal_to, regexp
-
-from flaskext.babel import gettext, lazy_gettext as _ 
-
-from newsmeme.models import User
-from newsmeme.extensions import db
-
-from .validators import is_username
-
-class LoginForm(Form):
-
-    next = HiddenField()
-    
-    remember = BooleanField(_("Remember me"))
-    
-    login = TextField(
-        _("Username or email address"), 
-        validators=[
-            required(
-                message=_("You must provide an email or username")
-            )
-        ]
-    )
-
-    password = PasswordField(_("Password"))
-
-    submit = SubmitField(_("Login"))
-
-class SignupForm(Form):
-
-    next = HiddenField()
-
-    username = TextField(_("Username"), validators=[
-                         required(message=_("Username required")), 
-                         is_username])
-
-    password = PasswordField(_("Password"), validators=[
-                             required(message=_("Password required"))])
-
-    password_again = PasswordField(_("Password again"), validators=[
-                                   equal_to("password", message=\
-                                            _("Passwords don't match"))])
-
-    email = TextField(_("Email address"), validators=[
-                      required(message=_("Email address required")), 
-                      email(message=_("A valid email address is required"))])
-
-    recaptcha = RecaptchaField(_("Copy the words appearing below"))
-
-    submit = SubmitField(_("Signup"))
-
-    def validate_username(self, field):
-        user = User.query.filter(User.username.like(field.data)).first()
-        if user:
-            raise ValidationError, gettext("This username is taken")
-
-    def validate_email(self, field):
-        user = User.query.filter(User.email.like(field.data)).first()
-        if user:
-            raise ValidationError, gettext("This email is taken")
-
-
-class EditAccountForm(Form):
-
-    username = TextField("Username", validators=[
-                         required(_("Username is required")), is_username])
-
-    email = TextField(_("Your email address"), validators=[
-                      required(message=_("Email address required")),
-                      email(message=_("A valid email address is required"))])
-
-    receive_email = BooleanField(_("Receive private emails from friends"))
-    
-    email_alerts = BooleanField(_("Receive an email when somebody replies "
-                                  "to your post or comment"))
-
-
-    submit = SubmitField(_("Save"))
-
-    def __init__(self, user, *args, **kwargs):
-        self.user = user
-        kwargs['obj'] = self.user
-        super(EditAccountForm, self).__init__(*args, **kwargs)
-        
-    def validate_username(self, field):
-        user = User.query.filter(db.and_(
-                                 User.username.like(field.data),
-                                 db.not_(User.id==self.user.id))).first()
-
-        if user:
-            raise ValidationError, gettext("This username is taken")
-
-    def validate_email(self, field):
-        user = User.query.filter(db.and_(
-                                 User.email.like(field.data),
-                                 db.not_(User.id==self.user.id))).first()
-        if user:
-            raise ValidationError, gettext("This email is taken")
-
-
-class RecoverPasswordForm(Form):
-
-    email = TextField("Your email address", validators=[
-                      email(message=_("A valid email address is required"))])
-
-    submit = SubmitField(_("Find password"))
-
-
-class ChangePasswordForm(Form):
-
-    activation_key = HiddenField()
-
-    password = PasswordField("Password", validators=[
-                             required(message=_("Password is required"))])
-    
-    password_again = PasswordField(_("Password again"), validators=[
-                                   equal_to("password", message=\
-                                            _("Passwords don't match"))])
-
-    submit = SubmitField(_("Save"))
-
-
-class DeleteAccountForm(Form):
-    
-    recaptcha = RecaptchaField(_("Copy the words appearing below"))
-
-    submit = SubmitField(_("Delete"))
-
-
+# -*- coding: utf-8 -*-
+from flask.ext.wtf import Form, HiddenField, BooleanField, TextField, \
+        PasswordField, SubmitField, TextField, RecaptchaField, \
+        ValidationError, required, email, equal_to, regexp
+
+from flask.ext.babel import gettext, lazy_gettext as _ 
+
+from newsmeme.models import User
+from newsmeme.extensions import db
+
+from .validators import is_username
+
+class LoginForm(Form):
+
+    next = HiddenField()
+    
+    remember = BooleanField(_("Remember me"))
+    
+    login = TextField(
+        _("Username or email address"), 
+        validators=[
+            required(
+                message=_("You must provide an email or username")
+            )
+        ]
+    )
+
+    password = PasswordField(_("Password"))
+
+    submit = SubmitField(_("Login"))
+
+class SignupForm(Form):
+
+    next = HiddenField()
+
+    username = TextField(_("Username"), validators=[
+                         required(message=_("Username required")), 
+                         is_username])
+
+    password = PasswordField(_("Password"), validators=[
+                             required(message=_("Password required"))])
+
+    password_again = PasswordField(_("Password again"), validators=[
+                                   equal_to("password", message=\
+                                            _("Passwords don't match"))])
+
+    email = TextField(_("Email address"), validators=[
+                      required(message=_("Email address required")), 
+                      email(message=_("A valid email address is required"))])
+
+    recaptcha = RecaptchaField(_("Copy the words appearing below"))
+
+    submit = SubmitField(_("Signup"))
+
+    def validate_username(self, field):
+        user = User.query.filter(User.username.like(field.data)).first()
+        if user:
+            raise ValidationError, gettext("This username is taken")
+
+    def validate_email(self, field):
+        user = User.query.filter(User.email.like(field.data)).first()
+        if user:
+            raise ValidationError, gettext("This email is taken")
+
+
+class EditAccountForm(Form):
+
+    username = TextField("Username", validators=[
+                         required(_("Username is required")), is_username])
+
+    email = TextField(_("Your email address"), validators=[
+                      required(message=_("Email address required")),
+                      email(message=_("A valid email address is required"))])
+
+    receive_email = BooleanField(_("Receive private emails from friends"))
+    
+    email_alerts = BooleanField(_("Receive an email when somebody replies "
+                                  "to your post or comment"))
+
+
+    submit = SubmitField(_("Save"))
+
+    def __init__(self, user, *args, **kwargs):
+        self.user = user
+        kwargs['obj'] = self.user
+        super(EditAccountForm, self).__init__(*args, **kwargs)
+        
+    def validate_username(self, field):
+        user = User.query.filter(db.and_(
+                                 User.username.like(field.data),
+                                 db.not_(User.id==self.user.id))).first()
+
+        if user:
+            raise ValidationError, gettext("This username is taken")
+
+    def validate_email(self, field):
+        user = User.query.filter(db.and_(
+                                 User.email.like(field.data),
+                                 db.not_(User.id==self.user.id))).first()
+        if user:
+            raise ValidationError, gettext("This email is taken")
+
+
+class RecoverPasswordForm(Form):
+
+    email = TextField("Your email address", validators=[
+                      email(message=_("A valid email address is required"))])
+
+    submit = SubmitField(_("Find password"))
+
+
+class ChangePasswordForm(Form):
+
+    activation_key = HiddenField()
+
+    password = PasswordField("Password", validators=[
+                             required(message=_("Password is required"))])
+    
+    password_again = PasswordField(_("Password again"), validators=[
+                                   equal_to("password", message=\
+                                            _("Passwords don't match"))])
+
+    submit = SubmitField(_("Save"))
+
+
+class DeleteAccountForm(Form):
+    
+    recaptcha = RecaptchaField(_("Copy the words appearing below"))
+
+    submit = SubmitField(_("Delete"))
+
+

newsmeme/forms/comment.py

-# -*- coding: utf-8 -*-
-from flaskext.wtf import Form, TextAreaField, SubmitField, required
-from flaskext.babel import lazy_gettext as _
-
-class CommentForm(Form):
-
-    comment = TextAreaField(validators=[
-                            required(message=_("Comment is required"))])
-
-    submit = SubmitField(_("Save"))
-    cancel = SubmitField(_("Cancel"))
-
-   
-class CommentAbuseForm(Form):
-
-    complaint = TextAreaField("Complaint", validators=[
-                              required(message=\
-                                       _("You must enter the details"
-                                         " of the complaint"))])
-
-
-    submit = SubmitField(_("Send"))
+# -*- coding: utf-8 -*-
+from flask.ext.wtf import Form, TextAreaField, SubmitField, required
+from flask.ext.babel import lazy_gettext as _
+
+class CommentForm(Form):
+
+    comment = TextAreaField(validators=[
+                            required(message=_("Comment is required"))])
+
+    submit = SubmitField(_("Save"))
+    cancel = SubmitField(_("Cancel"))
+
+   
+class CommentAbuseForm(Form):
+
+    complaint = TextAreaField("Complaint", validators=[
+                              required(message=\
+                                       _("You must enter the details"
+                                         " of the complaint"))])
+
+
+    submit = SubmitField(_("Send"))

newsmeme/forms/contact.py

-# -*- coding: utf-8 -*-
-from flaskext.wtf import Form, TextField, TextAreaField, SubmitField, \
-        required, email
-
-from flaskext.babel import lazy_gettext as _
-
-class ContactForm(Form):
-
-    name = TextField(_("Your name"), validators=[
-                     required(message=_('Your name is required'))])
-
-    email = TextField(_("Your email address"), validators=[
-                      required(message=_("Email address required")),
-                      email(message=_("A valid email address is required"))])
-
-    subject = TextField(_("Subject"), validators=[
-                        required(message=_("Subject required"))])
-
-    message = TextAreaField(_("Message"), validators=[
-                            required(message=_("Message required"))])
-
-    submit = SubmitField(_("Send"))
-
-class MessageForm(Form):
-
-    subject = TextField(_("Subject"), validators=[
-                        required(message=_("Subject required"))])
-
-    message = TextAreaField(_("Message"), validators=[
-                            required(message=_("Message required"))])
-
-    submit = SubmitField(_("Send"))
-
-
+# -*- coding: utf-8 -*-
+from flask.ext.wtf import Form, TextField, TextAreaField, SubmitField, \
+        required, email
+
+from flask.ext.babel import lazy_gettext as _
+
+class ContactForm(Form):
+
+    name = TextField(_("Your name"), validators=[
+                     required(message=_('Your name is required'))])
+
+    email = TextField(_("Your email address"), validators=[
+                      required(message=_("Email address required")),
+                      email(message=_("A valid email address is required"))])
+
+    subject = TextField(_("Subject"), validators=[
+                        required(message=_("Subject required"))])
+
+    message = TextAreaField(_("Message"), validators=[
+                            required(message=_("Message required"))])
+
+    submit = SubmitField(_("Send"))
+
+class MessageForm(Form):
+
+    subject = TextField(_("Subject"), validators=[
+                        required(message=_("Subject required"))])
+
+    message = TextAreaField(_("Message"), validators=[
+                            required(message=_("Message required"))])
+
+    submit = SubmitField(_("Send"))
+
+

newsmeme/forms/openid.py

-from flaskext.wtf import Form, HiddenField, TextField, RecaptchaField, \
-        SubmitField, ValidationError, required, email, url
-
-from flaskext.babel import lazy_gettext as _
-
-from newsmeme.models import User
-
-from .validators import is_username
-
-class OpenIdSignupForm(Form):
-    
-    next = HiddenField()
-
-    username = TextField(_("Username"), validators=[
-                         required(_("Username required")), 
-                         is_username])
-    
-    email = TextField(_("Email address"), validators=[
-                      required(message=_("Email address required")), 
-                      email(message=_("Valid email address required"))])
-    
-    recaptcha = RecaptchaField(_("Copy the words appearing below"))
-
-    submit = SubmitField(_("Signup"))
-
-    def validate_username(self, field):
-        user = User.query.filter(User.username.like(field.data)).first()
-        if user:
-            raise ValidationError, gettext("This username is taken")
-
-    def validate_email(self, field):
-        user = User.query.filter(User.email.like(field.data)).first()
-        if user:
-            raise ValidationError, gettext("This email is taken")
-
-class OpenIdLoginForm(Form):
-
-    next = HiddenField()
-
-    openid = TextField("OpenID", validators=[
-                       required(_("OpenID is required")), 
-                       url(_("OpenID must be a valid URL"))])
-
-    submit = SubmitField(_("Login"))
- 
+from flask.ext.wtf import Form, HiddenField, TextField, RecaptchaField, \
+        SubmitField, ValidationError, required, email, url
+
+from flask.ext.babel import lazy_gettext as _
+
+from newsmeme.models import User
+
+from .validators import is_username
+
+class OpenIdSignupForm(Form):
+    
+    next = HiddenField()
+
+    username = TextField(_("Username"), validators=[
+                         required(_("Username required")), 
+                         is_username])
+    
+    email = TextField(_("Email address"), validators=[
+                      required(message=_("Email address required")), 
+                      email(message=_("Valid email address required"))])
+    
+    recaptcha = RecaptchaField(_("Copy the words appearing below"))
+
+    submit = SubmitField(_("Signup"))
+
+    def validate_username(self, field):
+        user = User.query.filter(User.username.like(field.data)).first()
+        if user:
+            raise ValidationError, gettext("This username is taken")
+
+    def validate_email(self, field):
+        user = User.query.filter(User.email.like(field.data)).first()
+        if user:
+            raise ValidationError, gettext("This email is taken")
+
+class OpenIdLoginForm(Form):
+
+    next = HiddenField()
+
+    openid = TextField("OpenID", validators=[
+                       required(_("OpenID is required")), 
+                       url(_("OpenID must be a valid URL"))])
+
+    submit = SubmitField(_("Login"))
+ 

newsmeme/forms/post.py

-# -*- coding: utf-8 -*-
-from flaskext.wtf import Form, TextField, TextAreaField, RadioField, \
-        SubmitField, ValidationError, optional, required, url
-       
-from flaskext.babel import gettext, lazy_gettext as _
-
-from newsmeme.models import Post
-from newsmeme.extensions import db
-
-class PostForm(Form):
-
-    title = TextField(_("Title of your post"), validators=[
-                      required(message=_("Title required"))])
-
-    link = TextField(_("Link"), validators=[
-                     optional(),
-                     url(message=_("This is not a valid URL"))])
-
-    description = TextAreaField(_("Description"))
-
-    tags = TextField(_("Tags"))
-
-    access = RadioField(_("Who can see this post ?"), 
-                        default=Post.PUBLIC, 
-                        coerce=int,
-                        choices=((Post.PUBLIC, _("Everyone")),
-                                 (Post.FRIENDS, _("Friends only")),
-                                 (Post.PRIVATE, _("Just myself"))))
-
-    submit = SubmitField(_("Save"))
-
-    def __init__(self, *args, **kwargs):
-        self.post = kwargs.get('obj', None)
-        super(PostForm, self).__init__(*args, **kwargs)
-
-    def validate_link(self, field):
-        posts = Post.query.public().filter_by(link=field.data)
-        if self.post:
-            posts = posts.filter(db.not_(Post.id==self.post.id))
-        if posts.count():
-            raise ValidationError, gettext("This link has already been posted")
-
-
+# -*- coding: utf-8 -*-
+from flask.ext.wtf import Form, TextField, TextAreaField, RadioField, \
+        SubmitField, ValidationError, optional, required, url
+       
+from flask.ext.babel import gettext, lazy_gettext as _
+
+from newsmeme.models import Post
+from newsmeme.extensions import db
+
+class PostForm(Form):
+
+    title = TextField(_("Title of your post"), validators=[
+                      required(message=_("Title required"))])
+
+    link = TextField(_("Link"), validators=[
+                     optional(),
+                     url(message=_("This is not a valid URL"))])
+
+    description = TextAreaField(_("Description"))
+
+    tags = TextField(_("Tags"))
+
+    access = RadioField(_("Who can see this post ?"), 
+                        default=Post.PUBLIC, 
+                        coerce=int,
+                        choices=((Post.PUBLIC, _("Everyone")),
+                                 (Post.FRIENDS, _("Friends only")),
+                                 (Post.PRIVATE, _("Just myself"))))
+
+    submit = SubmitField(_("Save"))
+
+    def __init__(self, *args, **kwargs):
+        self.post = kwargs.get('obj', None)
+        super(PostForm, self).__init__(*args, **kwargs)
+
+    def validate_link(self, field):
+        posts = Post.query.public().filter_by(link=field.data)
+        if self.post:
+            posts = posts.filter(db.not_(Post.id==self.post.id))
+        if posts.count():
+            raise ValidationError, gettext("This link has already been posted")
+
+

newsmeme/forms/validators.py

-from flaskext.wtf import regexp
-
-from flaskext.babel import lazy_gettext as _
-
-USERNAME_RE = r'^[\w.+-]+$'
-
-is_username = regexp(USERNAME_RE, 
-                     message=_("You can only use letters, numbers or dashes"))
-
-
+from flask.ext.wtf import regexp
+
+from flask.ext.babel import lazy_gettext as _
+
+USERNAME_RE = r'^[\w.+-]+$'
+
+is_username = regexp(USERNAME_RE, 
+                     message=_("You can only use letters, numbers or dashes"))
+
+

newsmeme/helpers.py

-# -*- coding: utf-8 -*-
-"""
-    helpers.py
-    ~~~~~~~~
-
-    Helper functions for newsmeme
-
-    :copyright: (c) 2010 by Dan Jacob.
-    :license: BSD, see LICENSE for more details.
-"""
-import markdown
-import re
-import urlparse
-import functools
-
-from datetime import datetime
-
-from flask import current_app, g
-
-from flaskext.babel import gettext, ngettext
-from flaskext.themes import static_file_url, render_theme_template 
-
-from newsmeme.extensions import cache
-
-_punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
-
-def slugify(text, delim=u'-'):
-    """Generates an ASCII-only slug. From http://flask.pocoo.org/snippets/5/"""
-    result = []
-    for word in _punct_re.split(text.lower()):
-        #word = word.encode('translit/long')
-        if word:
-            result.append(word)
-    return unicode(delim.join(result))
-
-markdown = functools.partial(markdown.markdown,
-                             safe_mode='remove',
-                             output_format="html")
-
-
-cached = functools.partial(cache.cached,
-                           unless= lambda: g.user is not None)
-
-def get_theme():
-    return current_app.config['THEME']
-
-
-def render_template(template, **context):
-    return render_theme_template(get_theme(), template, **context)
-
-
-def timesince(dt, default=None):
-    """
-    Returns string representing "time since" e.g.
-    3 days ago, 5 hours ago etc.
-    NB: when/if Babel 1.0 releaseduse format_timedelta/timedeltaformat instead
-    """
-    
-    if default is None:
-        default = gettext("just now")
-
-    now = datetime.utcnow()
-    diff = now - dt
-    
-    years = diff.days / 365
-    months = diff.days / 30
-    weeks = diff.days / 7
-    days = diff.days
-    hours = diff.seconds / 3600
-    minutes = diff.seconds / 60
-    seconds = diff.seconds 
-
-    periods = (
-        (years, ngettext("%(num)s year", "%(num)s years", num=years)),
-        (months, ngettext("%(num)s month", "%(num)s months", num=months)),
-        (weeks, ngettext("%(num)s week", "%(num)s weeks", num=weeks)),
-        (days, ngettext("%(num)s day", "%(num)s days", num=days)),
-        (hours, ngettext("%(num)s hour", "%(num)s hours", num=hours)),
-        (minutes, ngettext("%(num)s minute", "%(num)s minutes", num=minutes)),
-        (seconds, ngettext("%(num)s second", "%(num)s seconds", num=seconds)),
-    )
-
-    for period, trans in periods:
-        if period:
-            return gettext("%(period)s ago", period=trans)
-
-    return default
-
-
-def domain(url):
-    """
-    Returns the domain of a URL e.g. http://reddit.com/ > reddit.com
-    """
-    rv = urlparse.urlparse(url).netloc
-    if rv.startswith("www."):
-        rv = rv[4:]
-    return rv
-
+# -*- coding: utf-8 -*-
+"""
+    helpers.py
+    ~~~~~~~~
+
+    Helper functions for newsmeme
+
+    :copyright: (c) 2010 by Dan Jacob.
+    :license: BSD, see LICENSE for more details.
+"""
+import markdown
+import re
+import urlparse
+import functools
+
+from datetime import datetime
+
+from flask import current_app, g
+
+from flask.ext.babel import gettext, ngettext
+from flask.ext.themes import static_file_url, render_theme_template 
+
+from newsmeme.extensions import cache
+
+_punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
+
+def slugify(text, delim=u'-'):
+    """Generates an ASCII-only slug. From http://flask.pocoo.org/snippets/5/"""
+    result = []
+    for word in _punct_re.split(text.lower()):
+        #word = word.encode('translit/long')
+        if word:
+            result.append(word)
+    return unicode(delim.join(result))
+
+markdown = functools.partial(markdown.markdown,
+                             safe_mode='remove',
+                             output_format="html")
+
+
+cached = functools.partial(cache.cached,
+                           unless= lambda: g.user is not None)
+
+def get_theme():
+    return current_app.config['THEME']
+
+
+def render_template(template, **context):
+    return render_theme_template(get_theme(), template, **context)
+
+
+def timesince(dt, default=None):
+    """
+    Returns string representing "time since" e.g.
+    3 days ago, 5 hours ago etc.
+    NB: when/if Babel 1.0 releaseduse format_timedelta/timedeltaformat instead
+    """
+    
+    if default is None:
+        default = gettext("just now")
+
+    now = datetime.utcnow()
+    diff = now - dt
+    
+    years = diff.days / 365
+    months = diff.days / 30
+    weeks = diff.days / 7
+    days = diff.days
+    hours = diff.seconds / 3600
+    minutes = diff.seconds / 60
+    seconds = diff.seconds 
+
+    periods = (
+        (years, ngettext("%(num)s year", "%(num)s years", num=years)),
+        (months, ngettext("%(num)s month", "%(num)s months", num=months)),
+        (weeks, ngettext("%(num)s week", "%(num)s weeks", num=weeks)),
+        (days, ngettext("%(num)s day", "%(num)s days", num=days)),
+        (hours, ngettext("%(num)s hour", "%(num)s hours", num=hours)),
+        (minutes, ngettext("%(num)s minute", "%(num)s minutes", num=minutes)),
+        (seconds, ngettext("%(num)s second", "%(num)s seconds", num=seconds)),
+    )
+
+    for period, trans in periods:
+        if period:
+            return gettext("%(period)s ago", period=trans)
+
+    return default
+
+
+def domain(url):
+    """
+    Returns the domain of a URL e.g. http://reddit.com/ > reddit.com
+    """
+    rv = urlparse.urlparse(url).netloc
+    if rv.startswith("www."):
+        rv = rv[4:]
+    return rv
+

newsmeme/models/__init__.py

-#-*- coding: utf-8 -*-
-"""
-    models.py
-    ~~~~~~~~~
-
-    newsmeme model code
-
-    :copyright: (c) 2010 by Dan Jacob.
-    :license: BSD, see LICENSE for more details.
-"""   
-
-from newsmeme.models.users import User
-from newsmeme.models.posts import Post, Tag, post_tags
-from newsmeme.models.comments import Comment
-
-
+#-*- coding: utf-8 -*-
+"""
+    models.py
+    ~~~~~~~~~
+
+    newsmeme model code
+
+    :copyright: (c) 2010 by Dan Jacob.
+    :license: BSD, see LICENSE for more details.
+"""   
+
+from newsmeme.models.users import User
+from newsmeme.models.posts import Post, Tag, post_tags
+from newsmeme.models.comments import Comment
+
+

newsmeme/models/base.py

-from mongoengine import QuerySet, DoesNotExist, MultipleObjectsReturned
-from flaskext.sqlalchemy import Pagination
-
-class BaseQuerySet(QuerySet):
-    """
-    Custom QuerySet with some handy extra methods. Use this or 
-    subclass of this with models:
-
-    class MyClass(Document):
-        ...
-        meta = {'queryset_class' : BaseQuerySet})
-    """
-
-    def get_or_404(self, **kwargs):
-        try:
-            return self.get(**kwargs)
-        except (DoesNotExist, MultipleObjectsReturned):
-            abort(404)
-
-    def first_or_404(self, **kwargs):
-        result = self.first()
-        if result is None:
-            abort(404)
-
-    def paginate(self, page, per_page=20, error_out=True):
-        """Returns `per_page` items from page `page`.  By default it will
-        abort with 404 if no items were found and the page was larger than
-        1.  This behavor can be disabled by setting `error_out` to `False`.
-
-        Returns an :class:`Pagination` object.
-        """
-        if error_out and page < 1:
-            abort(404)
-        items = self.limit(per_page).skip((page - 1) * per_page).all()
-        if not items and page != 1 and error_out:
-            abort(404)
-        return Pagination(self, page, per_page, self.count(), items)
+from mongoengine import QuerySet, DoesNotExist, MultipleObjectsReturned
+from flask.ext.sqlalchemy import Pagination
+
+class BaseQuerySet(QuerySet):
+    """
+    Custom QuerySet with some handy extra methods. Use this or 
+    subclass of this with models:
+
+    class MyClass(Document):
+        ...
+        meta = {'queryset_class' : BaseQuerySet})
+    """
+
+    def get_or_404(self, **kwargs):
+        try:
+            return self.get(**kwargs)
+        except (DoesNotExist, MultipleObjectsReturned):
+            abort(404)
+
+    def first_or_404(self, **kwargs):
+        result = self.first()
+        if result is None:
+            abort(404)
+
+    def paginate(self, page, per_page=20, error_out=True):
+        """Returns `per_page` items from page `page`.  By default it will
+        abort with 404 if no items were found and the page was larger than
+        1.  This behavor can be disabled by setting `error_out` to `False`.
+
+        Returns an :class:`Pagination` object.
+        """
+        if error_out and page < 1:
+            abort(404)
+        items = self.limit(per_page).skip((page - 1) * per_page).all()
+        if not items and page != 1 and error_out:
+            abort(404)
+        return Pagination(self, page, per_page, self.count(), items)

newsmeme/models/comments.py

-from datetime import datetime
-
-from werkzeug import cached_property
-
-from flask import Markup
-from flaskext.sqlalchemy import BaseQuery
-from flaskext.principal import Permission, UserNeed, Denial
-
-from newsmeme import signals
-from newsmeme.extensions import db
-from newsmeme.permissions import auth, moderator
-from newsmeme.helpers import markdown
-from newsmeme.models.posts import Post
-from newsmeme.models.users import User
-from newsmeme.models.permissions import Permissions
-from newsmeme.models.types import DenormalizedText
-
-class CommentQuery(BaseQuery):
-
-    def restricted(self, user):
-
-        if user and user.is_moderator:
-            return self
-       
-        q = self.join(Post)
-        criteria = [Post.access==Post.PUBLIC]
-
-        if user:
-            criteria.append(Post.author_id==user.id)
-            if user.friends:
-                criteria.append(db.and_(Post.access==Post.FRIENDS,
-                                        Post.author_id.in_(user.friends)))
-        
-        return q.filter(reduce(db.or_, criteria))
-
-   
-class Comment(db.Model):
-
-    __tablename__ = "comments"
-
-    PER_PAGE = 20
-
-    query_class = CommentQuery
-
-    id = db.Column(db.Integer, primary_key=True)
-    
-    author_id = db.Column(db.Integer, 
-                          db.ForeignKey(User.id, ondelete='CASCADE'), 
-                          nullable=False)
-
-    post_id = db.Column(db.Integer, 
-                        db.ForeignKey(Post.id, ondelete='CASCADE'), 
-                        nullable=False)
-
-    parent_id = db.Column(db.Integer, 
-                          db.ForeignKey("comments.id", ondelete='CASCADE'))
-
-    comment = db.Column(db.UnicodeText)
-    date_created = db.Column(db.DateTime, default=datetime.utcnow)
-    score = db.Column(db.Integer, default=1)
-    votes = db.Column(DenormalizedText)
-
-    author = db.relation(User, innerjoin=True, lazy="joined")
-
-    post = db.relation(Post, innerjoin=True, lazy="joined")
-
-    parent = db.relation('Comment', remote_side=[id])
-
-    __mapper_args__ = {'order_by' : id.asc()}
-    
-    class Permissions(Permissions):
-
-
-        @cached_property
-        def default(self):
-            return Permission(UserNeed(self.author_id)) & moderator
-
-        @cached_property
-        def edit(self):
-            return self.default
-
-        @cached_property
-        def delete(self):
-            return self.default
-
-        @cached_property
-        def vote(self):
-
-            needs = [UserNeed(user_id) for user_id in self.votes]
-            needs.append(UserNeed(self.author_id))
-
-            return auth & Denial(*needs)
-
-   
-    def __init__(self, *args, **kwargs):
-        super(Comment, self).__init__(*args, **kwargs)
-        self.votes = self.votes or set()
-
-    @cached_property
-    def permissions(self):
-        return self.Permissions(self)
-
-    def vote(self, user):
-        self.votes.add(user.id)
-
-    def _url(self, _external=False):
-        return '%s#comment-%d' % (self.post._url(_external), self.id)
-
-    @cached_property
-    def url(self):
-        return self._url()
-
-    @cached_property
-    def permalink(self):
-        return self._url(True)
-
-    @cached_property
-    def markdown(self):
-        return Markup(markdown(self.comment or ''))
-
-# ------------- SIGNALS ----------------#
-
-def update_num_comments(sender):
-    sender.num_comments = \
-        Comment.query.filter(Comment.post_id==sender.id).count()
-    
-    db.session.commit()
-
-
-signals.comment_added.connect(update_num_comments)
-signals.comment_deleted.connect(update_num_comments)
-
+from datetime import datetime
+
+from werkzeug import cached_property
+
+from flask import Markup
+from flask.ext.sqlalchemy import BaseQuery
+from flask.ext.principal import Permission, UserNeed, Denial
+
+from newsmeme import signals
+from newsmeme.extensions import db
+from newsmeme.permissions import auth, moderator
+from newsmeme.helpers import markdown
+from newsmeme.models.posts import Post
+from newsmeme.models.users import User
+from newsmeme.models.permissions import Permissions
+from newsmeme.models.types import DenormalizedText
+
+class CommentQuery(BaseQuery):
+
+    def restricted(self, user):
+
+        if user and user.is_moderator:
+            return self
+       
+        q = self.join(Post)
+        criteria = [Post.access==Post.PUBLIC]
+
+        if user:
+            criteria.append(Post.author_id==user.id)
+            if user.friends:
+                criteria.append(db.and_(Post.access==Post.FRIENDS,
+                                        Post.author_id.in_(user.friends)))
+        
+        return q.filter(reduce(db.or_, criteria))
+