use application factory, WIP

This commit is contained in:
Matthew Scragg 2014-10-21 16:06:27 -05:00
parent e6bc4928c9
commit 38e5ef85c0
17 changed files with 200 additions and 120 deletions

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
from realms.cli import cli from realms.commands import cli
if __name__ == '__main__': if __name__ == '__main__':
cli() cli()

View file

@ -60,14 +60,25 @@ class Application(Flask):
for module_name in self.config['MODULES']: for module_name in self.config['MODULES']:
sources = __import__('%s.%s' % (import_name, module_name), fromlist=fromlist) sources = __import__('%s.%s' % (import_name, module_name), fromlist=fromlist)
if hasattr(sources, 'init'):
sources.init(self)
# Blueprint # Blueprint
if hasattr(sources, 'views'): if hasattr(sources, 'views'):
self.register_blueprint(sources.views.blueprint) self.register_blueprint(sources.views.blueprint, url_prefix=self.config['RELATIVE_PATH'])
# Click # Click
if hasattr(sources, 'commands'): if hasattr(sources, 'commands'):
cli.add_command(sources.commands.cli, name=module_name) cli.add_command(sources.commands.cli, name=module_name)
# Hooks
if hasattr(sources, 'hooks'):
if hasattr(sources.hooks, 'before_request'):
self.before_request(sources.hooks.before_request)
if hasattr(sources.hooks, 'before_first_request'):
self.before_first_request(sources.hooks.before_first_request)
# print >> sys.stderr, ' * Ready in %.2fms' % (1000.0 * (time.time() - start_time)) # print >> sys.stderr, ' * Ready in %.2fms' % (1000.0 * (time.time() - start_time))
def make_response(self, rv): def make_response(self, rv):
@ -148,27 +159,29 @@ def error_handler(e):
return response, status_code return response, status_code
def create_app(config=None):
app = Application(__name__) app = Application(__name__)
app.config.from_object('realms.config') app.config.from_object('realms.config')
app.url_map.converters['regex'] = RegexConverter app.url_map.converters['regex'] = RegexConverter
app.url_map.strict_slashes = False app.url_map.strict_slashes = False
login_manager.init_app(app)
db.init_app(app)
cache.init_app(app)
assets.init_app(app)
for status_code in httplib.responses: for status_code in httplib.responses:
if status_code >= 400: if status_code >= 400:
app.register_error_handler(status_code, error_handler) app.register_error_handler(status_code, error_handler)
@app.before_request @app.before_request
def init_g(): def init_g():
g.assets = dict(css=['main.css'], js=['main.js']) g.assets = dict(css=['main.css'], js=['main.js'])
@app.template_filter('datetime') @app.template_filter('datetime')
def _jinja2_filter_datetime(ts): def _jinja2_filter_datetime(ts):
return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts)) return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts))
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(e):
return render_template('errors/404.html'), 404 return render_template('errors/404.html'), 404
@ -178,6 +191,14 @@ if app.config['RELATIVE_PATH']:
def root(): def root():
return redirect(url_for(app.config['ROOT_ENDPOINT'])) return redirect(url_for(app.config['ROOT_ENDPOINT']))
app.discover()
# This will be removed at some point
with app.app_context():
db.create_all()
return app
@click.group() @click.group()
@click.pass_context @click.pass_context
@ -191,13 +212,13 @@ def cli(ctx):
sys.exit() sys.exit()
# Init plugins here if possible # Init plugins here if possible
login_manager = LoginManager(app) login_manager = LoginManager()
login_manager.login_view = 'auth.login' login_manager.login_view = 'auth.login'
db = MySQLAlchemy(app) db = MySQLAlchemy()
cache = Cache(app) cache = Cache()
assets = Assets()
assets = Assets(app)
assets.register('main.js', assets.register('main.js',
'vendor/jquery/dist/jquery.js', 'vendor/jquery/dist/jquery.js',
'vendor/components-bootstrap/js/bootstrap.js', 'vendor/components-bootstrap/js/bootstrap.js',
@ -220,10 +241,7 @@ assets.register('main.css',
'vendor/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css', 'vendor/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css',
'css/style.css') 'css/style.css')
app.discover()
# This will be removed at some point
db.create_all()

View file

@ -1,5 +1,5 @@
from realms import config, app, db, cli from realms import config, create_app, db, cli
from realms.lib.util import random_string, in_virtualenv from realms.lib.util import random_string, in_virtualenv, green, yellow, red
from subprocess import call, Popen from subprocess import call, Popen
from multiprocessing import cpu_count from multiprocessing import cpu_count
import click import click
@ -8,6 +8,9 @@ import sys
import os import os
app = create_app()
def get_user(): def get_user():
for name in ('SUDO_USER', 'LOGNAME', 'USER', 'LNAME', 'USERNAME'): for name in ('SUDO_USER', 'LOGNAME', 'USER', 'LNAME', 'USERNAME'):
user = os.environ.get(name) user = os.environ.get(name)
@ -33,18 +36,6 @@ def module_exists(module_name):
return True return True
def green(s):
click.secho(s, fg='green')
def yellow(s):
click.secho(s, fg='yellow')
def red(s):
click.secho(s, fg='red')
@cli.command() @cli.command()
@click.option('--site-title', @click.option('--site-title',
default=config.SITE_TITLE, default=config.SITE_TITLE,
@ -243,7 +234,7 @@ def start_server():
green("Server started. Port: %s" % config.PORT) green("Server started. Port: %s" % config.PORT)
Popen('gunicorn realms:app -b 0.0.0.0:%s -k gevent %s' % Popen("gunicorn 'realms:create_app()' -b 0.0.0.0:%s -k gevent %s" %
(config.PORT, flags), shell=True, executable='/bin/bash') (config.PORT, flags), shell=True, executable='/bin/bash')

View file

@ -42,7 +42,6 @@ class HookMixin(object):
return f return f
return outer return outer
@classmethod @classmethod
def before(cls, method_name): def before(cls, method_name):
def outer(f, *args, **kwargs): def outer(f, *args, **kwargs):

View file

@ -1,7 +1,9 @@
import json import json
from realms import db
from sqlalchemy import not_ from sqlalchemy import not_
from datetime import datetime from datetime import datetime
from flask.ext.sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Model(db.Model): class Model(db.Model):

19
realms/lib/test.py Normal file
View file

@ -0,0 +1,19 @@
from flask.ext.testing import TestCase
from realms.lib.util import random_string
from realms import create_app
from subprocess import call
class BaseTest(TestCase):
def create_app(self):
app = create_app()
app.config['TESTING'] = True
app.config['PRESERVE_CONTEXT_ON_EXCEPTION'] = False
app.config['WIKI_PATH'] = '/tmp/%s' % random_string(12)
app.config['DB_URI'] = 'sqlite:////tmp/%s.db' % random_string(12)
return app
def tearDown(self):
call(['rm', '-rf', self.app.config['WIKI_PATH']])
call(['rm', '-f', self.app.config['DB_URI'][10:]])

View file

@ -1,3 +1,4 @@
import click
import re import re
import os import os
import hashlib import hashlib
@ -109,6 +110,18 @@ def is_su():
return os.geteuid() == 0 return os.geteuid() == 0
def green(s):
click.secho(s, fg='green')
def yellow(s):
click.secho(s, fg='yellow')
def red(s):
click.secho(s, fg='red')
def upstart_script(user='root', app_dir=None, port=5000, workers=2, path=None): def upstart_script(user='root', app_dir=None, port=5000, workers=2, path=None):
script = """ script = """
limit nofile 65335 65335 limit nofile 65335 65335

View file

@ -1,6 +1,7 @@
import click import click
from realms.lib.util import random_string from realms.lib.util import random_string
from realms.modules.auth.models import User from realms.modules.auth.models import User
from realms.lib.util import green, red, yellow
@click.group() @click.group()
@ -21,15 +22,15 @@ def create_user(username, email, password):
password = random_string(12) password = random_string(12)
if User.get_by_username(username): if User.get_by_username(username):
click.secho("Username %s already exists" % username, fg='red') red("Username %s already exists" % username)
return return
if User.get_by_email(email): if User.get_by_email(email):
click.secho("Email %s already exists" % email, fg='red') red("Email %s already exists" % email)
return return
User.create(username, email, password) User.create(username, email, password)
click.secho("User %s created" % username, fg='green') green("User %s created" % username)
if show_pass: if show_pass:
click.secho("Password: %s" % password, fg='yellow') yellow("Password: %s" % password)

View file

@ -1,6 +1,5 @@
from flask_wtf import Form, RecaptchaField from flask_wtf import Form
from wtforms import StringField, PasswordField, validators from wtforms import StringField, PasswordField, validators
from realms import app
class RegistrationForm(Form): class RegistrationForm(Form):
@ -12,9 +11,6 @@ class RegistrationForm(Form):
]) ])
confirm = PasswordField('Repeat Password') confirm = PasswordField('Repeat Password')
if app.config['RECAPTCHA_ENABLE']:
setattr(RegistrationForm, 'recaptcha', RecaptchaField("You Human?"))
class LoginForm(Form): class LoginForm(Form):
email = StringField('Email', [validators.DataRequired()]) email = StringField('Email', [validators.DataRequired()])

View file

@ -0,0 +1,8 @@
from flask import current_app
from flask_wtf import RecaptchaField
from .forms import RegistrationForm
def before_first_request():
if current_app.config['RECAPTCHA_ENABLE']:
setattr(RegistrationForm, 'recaptcha', RecaptchaField("You Human?"))

View file

@ -1,5 +1,6 @@
from flask import current_app
from flask.ext.login import UserMixin, logout_user, login_user, AnonymousUserMixin from flask.ext.login import UserMixin, logout_user, login_user, AnonymousUserMixin
from realms import config, login_manager, db from realms import login_manager, db
from realms.lib.model import Model from realms.lib.model import Model
from realms.lib.util import gravatar_url from realms.lib.util import gravatar_url
from itsdangerous import URLSafeSerializer, BadSignature from itsdangerous import URLSafeSerializer, BadSignature
@ -15,7 +16,7 @@ def load_user(user_id):
@login_manager.token_loader @login_manager.token_loader
def load_token(token): def load_token(token):
# Load unsafe because payload is needed for sig # Load unsafe because payload is needed for sig
sig_okay, payload = URLSafeSerializer(config.SECRET_KEY).loads_unsafe(token) sig_okay, payload = URLSafeSerializer(current_app.config['SECRET_KEY']).loads_unsafe(token)
if not payload: if not payload:
return None return None
@ -81,7 +82,7 @@ class User(Model, UserMixin):
""" """
Signed with app secret salted with sha256 of password hash of user (client secret) Signed with app secret salted with sha256 of password hash of user (client secret)
""" """
return URLSafeSerializer(config.SECRET_KEY + salt) return URLSafeSerializer(current_app.config['SECRET_KEY'] + salt)
@staticmethod @staticmethod
def auth(email, password): def auth(email, password):

View file

@ -1,16 +1,15 @@
from flask import g, render_template, request, redirect, Blueprint, flash, url_for from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for
from realms.modules.auth.models import User from realms.modules.auth.models import User
from realms.modules.auth.forms import LoginForm, RegistrationForm from realms.modules.auth.forms import LoginForm, RegistrationForm
from realms import app
blueprint = Blueprint('auth', __name__, url_prefix=app.config['RELATIVE_PATH']) blueprint = Blueprint('auth', __name__)
@blueprint.route("/logout") @blueprint.route("/logout")
def logout_page(): def logout_page():
User.logout() User.logout()
flash("You are now logged out") flash("You are now logged out")
return redirect(url_for(app.config['ROOT_ENDPOINT'])) return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
@blueprint.route("/login", methods=['GET', 'POST']) @blueprint.route("/login", methods=['GET', 'POST'])
@ -23,7 +22,7 @@ def login():
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
if User.auth(request.form['email'], request.form['password']): if User.auth(request.form['email'], request.form['password']):
return redirect(request.args.get("next") or url_for(app.config['ROOT_ENDPOINT'])) return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT']))
else: else:
flash('Email or Password Incorrect', 'warning') flash('Email or Password Incorrect', 'warning')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
@ -34,9 +33,9 @@ def login():
@blueprint.route("/register", methods=['GET', 'POST']) @blueprint.route("/register", methods=['GET', 'POST'])
def register(): def register():
if not app.config['REGISTRATION_ENABLED']: if not current_app.config['REGISTRATION_ENABLED']:
flash("Registration is disabled") flash("Registration is disabled")
return redirect(url_for(app.config['ROOT_ENDPOINT'])) return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
form = RegistrationForm() form = RegistrationForm()
@ -57,7 +56,7 @@ def register():
User.create(request.form['username'], request.form['email'], request.form['password']) User.create(request.form['username'], request.form['email'], request.form['password'])
User.auth(request.form['email'], request.form['password']) User.auth(request.form['email'], request.form['password'])
return redirect(request.args.get("next") or url_for(app.config['ROOT_ENDPOINT'])) return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT']))
return render_template("auth/register.html", form=form) return render_template("auth/register.html", form=form)

View file

@ -1,8 +1,9 @@
import os import os
import sys import sys
from realms import app
from realms.modules.wiki.models import Wiki from realms.modules.wiki.models import Wiki
def init(app):
# Init Wiki # Init Wiki
Wiki(app.config['WIKI_PATH']) Wiki(app.config['WIKI_PATH'])

View file

@ -0,0 +1,6 @@
from flask import g, current_app
from .models import Wiki
def before_request():
g.current_wiki = Wiki(current_app.config['WIKI_PATH'])

View file

@ -62,6 +62,15 @@ class Wiki(HookMixin):
def __repr__(self): def __repr__(self):
return "Wiki: %s" % self.path return "Wiki: %s" % self.path
def _get_user(self, username, email):
if not username:
username = self.default_committer_name
if not email:
email = self.default_committer_email
return username, email
def revert_page(self, name, commit_sha, message, username): def revert_page(self, name, commit_sha, message, username):
"""Revert page to passed commit sha1 """Revert page to passed commit sha1
@ -108,11 +117,7 @@ class Wiki(HookMixin):
if not message: if not message:
message = "Updated %s" % name message = "Updated %s" % name
if not username: username, email = self._get_user(username, email)
username = self.default_committer_name
if not email:
email = self.default_committer_email
ret = self.gittle.commit(name=username, ret = self.gittle.commit(name=username,
email=email, email=email,
@ -168,12 +173,13 @@ class Wiki(HookMixin):
content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL) content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL)
return content return content
def rename_page(self, old_name, new_name, user=None): def rename_page(self, old_name, new_name, username=None, email=None, message=None):
"""Rename page. """Rename page.
:param old_name: Page that will be renamed. :param old_name: Page that will be renamed.
:param new_name: New name of page. :param new_name: New name of page.
:param user: User object if any. :param username: Committer name
:param email: Committer email
:return: str -- Commit sha1 :return: str -- Commit sha1
""" """
@ -186,32 +192,45 @@ class Wiki(HookMixin):
# file is being overwritten, but that is ok, it's git! # file is being overwritten, but that is ok, it's git!
pass pass
username, email = self._get_user(username, email)
if not message:
message = "Moved %s to %s" % (old_name, new_name)
os.rename(os.path.join(self.path, old_filename), os.path.join(self.path, new_filename)) os.rename(os.path.join(self.path, old_filename), os.path.join(self.path, new_filename))
self.gittle.add(new_filename) self.gittle.add(new_filename)
self.gittle.rm(old_filename) self.gittle.rm(old_filename)
commit = self.gittle.commit(name=getattr(user, 'username', self.default_committer_name), commit = self.gittle.commit(name=username,
email=getattr(user, 'email', self.default_committer_email), email=email,
message="Moved %s to %s" % (old_name, new_name), message=message,
files=[old_filename, new_filename]) files=[old_filename, new_filename])
cache.delete_many(old_filename, new_filename) cache.delete_many(old_name, new_name)
return commit return commit
def delete_page(self, name, user=None): def delete_page(self, name, username=None, email=None, message=None):
"""Delete page. """Delete page.
:param name: Page that will be deleted :param name: Page that will be deleted
:param user: User object if any :param username: Committer name
:param email: Committer email
:return: str -- Commit sha1 :return: str -- Commit sha1
""" """
self.gittle.rm(name)
commit = self.gittle.commit(name=getattr(user, 'username', self.default_committer_name), username, email = self._get_user(username, email)
email=getattr(user, 'email', self.default_committer_email),
message="Deleted %s" % name, if not message:
files=[name]) message = "Deleted %s" % name
filename = cname_to_filename(name)
self.gittle.rm(filename)
commit = self.gittle.commit(name=username,
email=email,
message=message,
files=[str(filename)])
cache.delete_many(name) cache.delete_many(name)
return commit return commit

View file

@ -1,15 +1,10 @@
from nose.tools import * from nose.tools import *
from flask import url_for from flask import url_for
from realms import app, g
from realms.modules.wiki.models import Wiki, cname_to_filename, filename_to_cname from realms.modules.wiki.models import Wiki, cname_to_filename, filename_to_cname
from flask.ext.testing import TestCase from realms.lib.test import BaseTest
class WikiTest(TestCase): class WikiTest(BaseTest):
def create_app(self):
app.config['TESTING'] = True
return app
def test_wiki_routes(self): def test_wiki_routes(self):
@ -22,7 +17,17 @@ class WikiTest(TestCase):
""" """
def test_write_page(self): def test_write_page(self):
pass self.assert_200(
self.client.post(url_for('wiki.page_write', name='test'), data=dict(
content='testing',
message='test message'
)))
self.assert_200(self.client.get(url_for('wiki.page', name='test')))
def test_delete_page(self):
self.assert_200(self.client.delete(url_for('wiki.page_write', name='test')))
self.assert_status(self.client.get(url_for('wiki.page', name='test')), 302)
def test_revert(self): def test_revert(self):
pass pass

View file

@ -1,15 +1,9 @@
from flask import abort, g, render_template, request, redirect, Blueprint, flash, url_for, current_app from flask import abort, g, render_template, request, redirect, Blueprint, flash, url_for, current_app
from flask.ext.login import login_required from flask.ext.login import login_required, current_user
from realms.lib.util import to_canonical, remove_ext from realms.lib.util import to_canonical, remove_ext
from realms.modules.wiki.models import Wiki
from realms import current_user, app
blueprint = Blueprint('wiki', __name__, url_prefix=app.config['RELATIVE_PATH'])
@app.before_request blueprint = Blueprint('wiki', __name__)
def init_wiki():
g.current_wiki = Wiki(app.config['WIKI_PATH'])
@blueprint.route("/_commit/<sha>/<name>") @blueprint.route("/_commit/<sha>/<name>")
@ -27,7 +21,8 @@ def commit(name, sha):
@blueprint.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>") @blueprint.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
def compare(name, fsha, dots, lsha): def compare(name, fsha, dots, lsha):
diff = g.current_wiki.compare(name, fsha, lsha) diff = g.current_wiki.compare(name, fsha, lsha)
return render_template('wiki/compare.html', name=name, diff=diff, old=fsha, new=lsha) return render_template('wiki/compare.html',
name=name, diff=diff, old=fsha, new=lsha)
@blueprint.route("/_revert", methods=['POST']) @blueprint.route("/_revert", methods=['POST'])
@ -38,11 +33,14 @@ def revert():
cname = to_canonical(name) cname = to_canonical(name)
message = request.form.get('message', "Reverting %s" % cname) message = request.form.get('message', "Reverting %s" % cname)
if cname in app.config.get('WIKI_LOCKED_PAGES'): if cname in current_app.config.get('WIKI_LOCKED_PAGES'):
return dict(error=True, message="Page is locked") return dict(error=True, message="Page is locked")
sha = g.current_wiki.revert_page(name, commit, message=message, sha = g.current_wiki.revert_page(name,
username=current_user.username) commit,
message=message,
username=current_user.username,
email=current_user.email)
if sha: if sha:
flash("Page reverted") flash("Page reverted")
@ -105,19 +103,20 @@ def page_write(name):
if request.method == 'POST': if request.method == 'POST':
# Create # Create
if cname in app.config.get('WIKI_LOCKED_PAGES'): if cname in current_app.config.get('WIKI_LOCKED_PAGES'):
return dict(error=True, message="Page is locked") return dict(error=True, message="Page is locked")
sha = g.current_wiki.write_page(cname, sha = g.current_wiki.write_page(cname,
request.form['content'], request.form['content'],
message=request.form['message'], message=request.form['message'],
create=True, create=True,
username=current_user.username) username=current_user.username,
email=current_user.email)
elif request.method == 'PUT': elif request.method == 'PUT':
edit_cname = to_canonical(request.form['name']) edit_cname = to_canonical(request.form['name'])
if edit_cname in app.config.get('WIKI_LOCKED_PAGES'): if edit_cname in current_app.config.get('WIKI_LOCKED_PAGES'):
return dict(error=True, message="Page is locked") return dict(error=True, message="Page is locked")
if edit_cname != cname.lower(): if edit_cname != cname.lower():
@ -126,13 +125,16 @@ def page_write(name):
sha = g.current_wiki.write_page(edit_cname, sha = g.current_wiki.write_page(edit_cname,
request.form['content'], request.form['content'],
message=request.form['message'], message=request.form['message'],
username=current_user.username) username=current_user.username,
email=current_user.email)
return dict(sha=sha) return dict(sha=sha)
else: else:
# DELETE # DELETE
sha = g.current_wiki.delete_page(name, user=current_user) sha = g.current_wiki.delete_page(name,
username=current_user.username,
email=current_user.email)
return dict(sha=sha) return dict(sha=sha)