use application factory, WIP
This commit is contained in:
parent
e6bc4928c9
commit
38e5ef85c0
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from realms.cli import cli
|
||||
from realms.commands import cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
|
|
|
@ -60,14 +60,25 @@ class Application(Flask):
|
|||
for module_name in self.config['MODULES']:
|
||||
sources = __import__('%s.%s' % (import_name, module_name), fromlist=fromlist)
|
||||
|
||||
if hasattr(sources, 'init'):
|
||||
sources.init(self)
|
||||
|
||||
# Blueprint
|
||||
if hasattr(sources, 'views'):
|
||||
self.register_blueprint(sources.views.blueprint)
|
||||
self.register_blueprint(sources.views.blueprint, url_prefix=self.config['RELATIVE_PATH'])
|
||||
|
||||
# Click
|
||||
if hasattr(sources, 'commands'):
|
||||
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))
|
||||
|
||||
def make_response(self, rv):
|
||||
|
@ -148,27 +159,29 @@ def error_handler(e):
|
|||
return response, status_code
|
||||
|
||||
|
||||
|
||||
def create_app(config=None):
|
||||
app = Application(__name__)
|
||||
app.config.from_object('realms.config')
|
||||
app.url_map.converters['regex'] = RegexConverter
|
||||
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:
|
||||
if status_code >= 400:
|
||||
app.register_error_handler(status_code, error_handler)
|
||||
|
||||
|
||||
@app.before_request
|
||||
def init_g():
|
||||
g.assets = dict(css=['main.css'], js=['main.js'])
|
||||
|
||||
|
||||
@app.template_filter('datetime')
|
||||
def _jinja2_filter_datetime(ts):
|
||||
return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts))
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('errors/404.html'), 404
|
||||
|
@ -178,6 +191,14 @@ if app.config['RELATIVE_PATH']:
|
|||
def root():
|
||||
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.pass_context
|
||||
|
@ -191,13 +212,13 @@ def cli(ctx):
|
|||
sys.exit()
|
||||
|
||||
# Init plugins here if possible
|
||||
login_manager = LoginManager(app)
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'auth.login'
|
||||
|
||||
db = MySQLAlchemy(app)
|
||||
cache = Cache(app)
|
||||
db = MySQLAlchemy()
|
||||
cache = Cache()
|
||||
assets = Assets()
|
||||
|
||||
assets = Assets(app)
|
||||
assets.register('main.js',
|
||||
'vendor/jquery/dist/jquery.js',
|
||||
'vendor/components-bootstrap/js/bootstrap.js',
|
||||
|
@ -220,10 +241,7 @@ assets.register('main.css',
|
|||
'vendor/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css',
|
||||
'css/style.css')
|
||||
|
||||
app.discover()
|
||||
|
||||
# This will be removed at some point
|
||||
db.create_all()
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from realms import config, app, db, cli
|
||||
from realms.lib.util import random_string, in_virtualenv
|
||||
from realms import config, create_app, db, cli
|
||||
from realms.lib.util import random_string, in_virtualenv, green, yellow, red
|
||||
from subprocess import call, Popen
|
||||
from multiprocessing import cpu_count
|
||||
import click
|
||||
|
@ -8,6 +8,9 @@ import sys
|
|||
import os
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
def get_user():
|
||||
for name in ('SUDO_USER', 'LOGNAME', 'USER', 'LNAME', 'USERNAME'):
|
||||
user = os.environ.get(name)
|
||||
|
@ -33,18 +36,6 @@ def module_exists(module_name):
|
|||
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()
|
||||
@click.option('--site-title',
|
||||
default=config.SITE_TITLE,
|
||||
|
@ -243,7 +234,7 @@ def start_server():
|
|||
|
||||
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')
|
||||
|
||||
|
|
@ -42,7 +42,6 @@ class HookMixin(object):
|
|||
return f
|
||||
return outer
|
||||
|
||||
|
||||
@classmethod
|
||||
def before(cls, method_name):
|
||||
def outer(f, *args, **kwargs):
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import json
|
||||
from realms import db
|
||||
from sqlalchemy import not_
|
||||
from datetime import datetime
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
class Model(db.Model):
|
||||
|
|
19
realms/lib/test.py
Normal file
19
realms/lib/test.py
Normal 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:]])
|
|
@ -1,3 +1,4 @@
|
|||
import click
|
||||
import re
|
||||
import os
|
||||
import hashlib
|
||||
|
@ -109,6 +110,18 @@ def is_su():
|
|||
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):
|
||||
script = """
|
||||
limit nofile 65335 65335
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import click
|
||||
from realms.lib.util import random_string
|
||||
from realms.modules.auth.models import User
|
||||
from realms.lib.util import green, red, yellow
|
||||
|
||||
|
||||
@click.group()
|
||||
|
@ -21,15 +22,15 @@ def create_user(username, email, password):
|
|||
password = random_string(12)
|
||||
|
||||
if User.get_by_username(username):
|
||||
click.secho("Username %s already exists" % username, fg='red')
|
||||
red("Username %s already exists" % username)
|
||||
return
|
||||
|
||||
if User.get_by_email(email):
|
||||
click.secho("Email %s already exists" % email, fg='red')
|
||||
red("Email %s already exists" % email)
|
||||
return
|
||||
|
||||
User.create(username, email, password)
|
||||
click.secho("User %s created" % username, fg='green')
|
||||
green("User %s created" % username)
|
||||
|
||||
if show_pass:
|
||||
click.secho("Password: %s" % password, fg='yellow')
|
||||
yellow("Password: %s" % password)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from flask_wtf import Form, RecaptchaField
|
||||
from flask_wtf import Form
|
||||
from wtforms import StringField, PasswordField, validators
|
||||
from realms import app
|
||||
|
||||
|
||||
class RegistrationForm(Form):
|
||||
|
@ -12,9 +11,6 @@ class RegistrationForm(Form):
|
|||
])
|
||||
confirm = PasswordField('Repeat Password')
|
||||
|
||||
if app.config['RECAPTCHA_ENABLE']:
|
||||
setattr(RegistrationForm, 'recaptcha', RecaptchaField("You Human?"))
|
||||
|
||||
|
||||
class LoginForm(Form):
|
||||
email = StringField('Email', [validators.DataRequired()])
|
||||
|
|
8
realms/modules/auth/hooks.py
Normal file
8
realms/modules/auth/hooks.py
Normal 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?"))
|
|
@ -1,5 +1,6 @@
|
|||
from flask import current_app
|
||||
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.util import gravatar_url
|
||||
from itsdangerous import URLSafeSerializer, BadSignature
|
||||
|
@ -15,7 +16,7 @@ def load_user(user_id):
|
|||
@login_manager.token_loader
|
||||
def load_token(token):
|
||||
# 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:
|
||||
return None
|
||||
|
@ -81,7 +82,7 @@ class User(Model, UserMixin):
|
|||
"""
|
||||
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
|
||||
def auth(email, password):
|
||||
|
|
|
@ -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.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")
|
||||
def logout_page():
|
||||
User.logout()
|
||||
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'])
|
||||
|
@ -23,7 +22,7 @@ def login():
|
|||
return redirect(url_for('auth.login'))
|
||||
|
||||
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:
|
||||
flash('Email or Password Incorrect', 'warning')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
@ -34,9 +33,9 @@ def login():
|
|||
@blueprint.route("/register", methods=['GET', 'POST'])
|
||||
def register():
|
||||
|
||||
if not app.config['REGISTRATION_ENABLED']:
|
||||
if not current_app.config['REGISTRATION_ENABLED']:
|
||||
flash("Registration is disabled")
|
||||
return redirect(url_for(app.config['ROOT_ENDPOINT']))
|
||||
return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
|
||||
|
||||
form = RegistrationForm()
|
||||
|
||||
|
@ -57,7 +56,7 @@ def register():
|
|||
User.create(request.form['username'], 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)
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import os
|
||||
import sys
|
||||
from realms import app
|
||||
from realms.modules.wiki.models import Wiki
|
||||
|
||||
|
||||
def init(app):
|
||||
# Init Wiki
|
||||
Wiki(app.config['WIKI_PATH'])
|
||||
|
||||
|
|
6
realms/modules/wiki/hooks.py
Normal file
6
realms/modules/wiki/hooks.py
Normal 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'])
|
|
@ -62,6 +62,15 @@ class Wiki(HookMixin):
|
|||
def __repr__(self):
|
||||
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):
|
||||
"""Revert page to passed commit sha1
|
||||
|
||||
|
@ -108,11 +117,7 @@ class Wiki(HookMixin):
|
|||
if not message:
|
||||
message = "Updated %s" % name
|
||||
|
||||
if not username:
|
||||
username = self.default_committer_name
|
||||
|
||||
if not email:
|
||||
email = self.default_committer_email
|
||||
username, email = self._get_user(username, email)
|
||||
|
||||
ret = self.gittle.commit(name=username,
|
||||
email=email,
|
||||
|
@ -168,12 +173,13 @@ class Wiki(HookMixin):
|
|||
content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL)
|
||||
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.
|
||||
|
||||
:param old_name: Page that will be renamed.
|
||||
: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
|
||||
|
||||
"""
|
||||
|
@ -186,32 +192,45 @@ class Wiki(HookMixin):
|
|||
# file is being overwritten, but that is ok, it's git!
|
||||
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))
|
||||
|
||||
self.gittle.add(new_filename)
|
||||
self.gittle.rm(old_filename)
|
||||
|
||||
commit = self.gittle.commit(name=getattr(user, 'username', self.default_committer_name),
|
||||
email=getattr(user, 'email', self.default_committer_email),
|
||||
message="Moved %s to %s" % (old_name, new_name),
|
||||
commit = self.gittle.commit(name=username,
|
||||
email=email,
|
||||
message=message,
|
||||
files=[old_filename, new_filename])
|
||||
|
||||
cache.delete_many(old_filename, new_filename)
|
||||
cache.delete_many(old_name, new_name)
|
||||
|
||||
return commit
|
||||
|
||||
def delete_page(self, name, user=None):
|
||||
def delete_page(self, name, username=None, email=None, message=None):
|
||||
"""Delete page.
|
||||
: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
|
||||
|
||||
"""
|
||||
self.gittle.rm(name)
|
||||
commit = self.gittle.commit(name=getattr(user, 'username', self.default_committer_name),
|
||||
email=getattr(user, 'email', self.default_committer_email),
|
||||
message="Deleted %s" % name,
|
||||
files=[name])
|
||||
|
||||
username, email = self._get_user(username, email)
|
||||
|
||||
if not message:
|
||||
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)
|
||||
return commit
|
||||
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
from nose.tools import *
|
||||
from flask import url_for
|
||||
from realms import app, g
|
||||
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):
|
||||
|
||||
def create_app(self):
|
||||
app.config['TESTING'] = True
|
||||
return app
|
||||
class WikiTest(BaseTest):
|
||||
|
||||
def test_wiki_routes(self):
|
||||
|
||||
|
@ -22,7 +17,17 @@ class WikiTest(TestCase):
|
|||
"""
|
||||
|
||||
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):
|
||||
pass
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
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.modules.wiki.models import Wiki
|
||||
from realms import current_user, app
|
||||
|
||||
blueprint = Blueprint('wiki', __name__, url_prefix=app.config['RELATIVE_PATH'])
|
||||
|
||||
|
||||
@app.before_request
|
||||
def init_wiki():
|
||||
g.current_wiki = Wiki(app.config['WIKI_PATH'])
|
||||
blueprint = Blueprint('wiki', __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>")
|
||||
def compare(name, fsha, dots, 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'])
|
||||
|
@ -38,11 +33,14 @@ def revert():
|
|||
cname = to_canonical(name)
|
||||
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")
|
||||
|
||||
sha = g.current_wiki.revert_page(name, commit, message=message,
|
||||
username=current_user.username)
|
||||
sha = g.current_wiki.revert_page(name,
|
||||
commit,
|
||||
message=message,
|
||||
username=current_user.username,
|
||||
email=current_user.email)
|
||||
if sha:
|
||||
flash("Page reverted")
|
||||
|
||||
|
@ -105,19 +103,20 @@ def page_write(name):
|
|||
|
||||
if request.method == 'POST':
|
||||
# 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")
|
||||
|
||||
sha = g.current_wiki.write_page(cname,
|
||||
request.form['content'],
|
||||
message=request.form['message'],
|
||||
create=True,
|
||||
username=current_user.username)
|
||||
username=current_user.username,
|
||||
email=current_user.email)
|
||||
|
||||
elif request.method == 'PUT':
|
||||
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")
|
||||
|
||||
if edit_cname != cname.lower():
|
||||
|
@ -126,13 +125,16 @@ def page_write(name):
|
|||
sha = g.current_wiki.write_page(edit_cname,
|
||||
request.form['content'],
|
||||
message=request.form['message'],
|
||||
username=current_user.username)
|
||||
username=current_user.username,
|
||||
email=current_user.email)
|
||||
|
||||
return dict(sha=sha)
|
||||
|
||||
else:
|
||||
# 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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue