Merge pull request #31 from scragg0x/app_factory

App factory
This commit is contained in:
Matthew Scragg 2014-10-22 15:43:49 -05:00
commit 9de1e4bdd8
23 changed files with 354 additions and 216 deletions

View file

@ -1,4 +1,5 @@
include requirements.txt VERSION LICENSE README.md include requirements.txt LICENSE README.md
recursive-include realms/static/img *
recursive-include realms/static/fonts * recursive-include realms/static/fonts *
recursive-include realms/static/css * recursive-include realms/static/css *
recursive-include realms/static/js * recursive-include realms/static/js *

View file

@ -1 +0,0 @@
0.3.20

View file

@ -1,6 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
import pip
from realms.cli import cli from realms.cli import cli
if __name__ == '__main__': if __name__ == '__main__':
print
print "----------------------------------"
print "This script is for development.\n" \
"If you installed via pip, " \
"you should have realms-wiki in your PATH that should be used instead."
print "----------------------------------"
print
cli() cli()

View file

@ -18,9 +18,10 @@ from flask.ext.assets import Environment, Bundle
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from realms.lib.util import to_canonical, remove_ext, mkdir_safe, gravatar_url, to_dict from .lib.util import to_canonical, remove_ext, mkdir_safe, gravatar_url, to_dict
from realms.lib.hook import HookModelMeta from .lib.hook import HookModelMeta
from realms.lib.util import is_su, in_virtualenv from .lib.util import is_su, in_virtualenv
from .version import __version__
class Application(Flask): class Application(Flask):
@ -60,15 +61,26 @@ 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)
# print >> sys.stderr, ' * Ready in %.2fms' % (1000.0 * (time.time() - start_time)) # 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): def make_response(self, rv):
if rv is None: if rv is None:
@ -148,56 +160,55 @@ def error_handler(e):
return response, status_code 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
app = Application(__name__) login_manager.init_app(app)
app.config.from_object('realms.config') db.init_app(app)
app.url_map.converters['regex'] = RegexConverter cache.init_app(app)
app.url_map.strict_slashes = False 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
def init_g():
g.assets = dict(css=['main.css'], js=['main.js'])
@app.before_request @app.template_filter('datetime')
def init_g(): def _jinja2_filter_datetime(ts):
g.assets = dict(css=['main.css'], js=['main.js']) 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
@app.template_filter('datetime') if app.config['RELATIVE_PATH']:
def _jinja2_filter_datetime(ts): @app.route("/")
return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts)) def root():
return redirect(url_for(app.config['ROOT_ENDPOINT']))
app.discover()
@app.errorhandler(404) # This will be removed at some point
def page_not_found(e): with app.app_context():
return render_template('errors/404.html'), 404 db.create_all()
if app.config['RELATIVE_PATH']: return app
@app.route("/")
def root():
return redirect(url_for(app.config['ROOT_ENDPOINT']))
@click.group()
@click.pass_context
def cli(ctx):
# This could probably done better
if ctx.invoked_subcommand in ['setup', 'setup_upstart', 'pip']:
if not in_virtualenv() and not is_su():
# This does not account for people the have user level python installs
# that aren't virtual environments! Should be rare I think
click.secho("This command requires root privileges, use sudo or run as root.", fg='red')
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,13 +231,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()
@click.group()
def cli():
pass

View file

@ -1,11 +1,40 @@
from realms import config, app, db, cli from realms import config, create_app, db, cli as cli_, __version__
from realms.lib.util import random_string, in_virtualenv from realms.lib.util import is_su, 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
import json import json
import sys import sys
import os import os
import pip
def print_version(ctx, param, value):
if not value or ctx.resilient_parsing:
return
green(__version__)
ctx.exit()
@click.group()
@click.option('--version', is_flag=True, callback=print_version,
expose_value=False, is_eager=True)
@click.pass_context
def cli(ctx):
# This could probably done better
if ctx.invoked_subcommand in ['setup', 'pip']:
if not in_virtualenv() and not is_su():
# This does not account for people the have user level python installs
# that aren't virtual environments! Should be rare I think
click.secho("This command requires root privileges, use sudo or run as root.", fg='red')
sys.exit()
if ctx.invoked_subcommand in ['setup_upstart']:
if not is_su():
click.secho("This command requires root privileges, use sudo or run as root.", fg='red')
sys.exit()
cli.add_command(cli_)
def get_user(): def get_user():
@ -33,18 +62,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,
@ -126,37 +143,28 @@ def get_prefix():
return sys.prefix return sys.prefix
def get_pip(): @cli.command(name='pip')
""" Get virtualenv path for pip
"""
if in_virtualenv():
return get_prefix() + '/bin/pip'
else:
return 'pip'
@cli.command()
@click.argument('cmd', nargs=-1) @click.argument('cmd', nargs=-1)
def pip(cmd): def pip_(cmd):
""" Execute pip commands, useful for virtualenvs """ Execute pip commands, useful for virtualenvs
""" """
call(get_pip() + ' ' + ' '.join(cmd), shell=True) pip.main(cmd)
def install_redis(): def install_redis():
call([get_pip(), 'install', 'redis']) pip.main(['install', 'redis'])
def install_mysql(): def install_mysql():
call([get_pip(), 'install', 'MySQL-Python']) pip.main(['install', 'MySQL-Python'])
def install_postgres(): def install_postgres():
call([get_pip(), 'install', 'psycopg2']) pip.main(['install', 'psycopg2'])
def install_memcached(): def install_memcached():
call([get_pip(), 'install', 'python-memcached']) pip.main(['install', 'python-memcached'])
@click.command() @click.command()
@ -229,9 +237,9 @@ def dev(port):
""" Run development server """ Run development server
""" """
green("Starting development server") green("Starting development server")
app.run(host="0.0.0.0", create_app().run(host="0.0.0.0",
port=port, port=port,
debug=True) debug=True)
def start_server(): def start_server():
@ -243,8 +251,8 @@ 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')
def stop_server(): def stop_server():
@ -317,21 +325,20 @@ def drop_db():
def test(): def test():
""" Run tests """ Run tests
""" """
for mod in [('flask.ext.testing', 'Flask-Testing'), ('nose', 'nose')]: for mod in [('flask.ext.testing', 'Flask-Testing'), ('nose', 'nose'), ('blinker', 'blinker')]:
if not module_exists(mod[0]): if not module_exists(mod[0]):
call([get_pip(), 'install', mod[1]]) pip.main(['install', mod[1]])
nosetests = get_prefix() + "/bin/nosetests" if in_virtualenv() else "nosetests" nosetests = get_prefix() + "/bin/nosetests" if in_virtualenv() else "nosetests"
call([nosetests, config.APP_PATH]) call([nosetests, 'realms'])
@cli.command() @cli.command()
def version(): def version():
""" Output version """ Output version
""" """
with open(os.path.join(config.APP_PATH, 'VERSION')) as f: green(__version__)
click.echo(f.read().strip())
if __name__ == '__main__': if __name__ == '__main__':

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):

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

@ -0,0 +1,23 @@
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)
app.config.update(self.configure())
return app
def configure(self):
return {}
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,13 +1,14 @@
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
# Init Wiki
Wiki(app.config['WIKI_PATH'])
# Check paths def init(app):
for mode in [os.W_OK, os.R_OK]: # Init Wiki
for dir_ in [app.config['WIKI_PATH'], os.path.join(app.config['WIKI_PATH'], '.git')]: Wiki(app.config['WIKI_PATH'])
if not os.access(dir_, mode):
sys.exit('Read and write access to WIKI_PATH is required (%s)' % dir_) # Check paths
for mode in [os.W_OK, os.R_OK]:
for dir_ in [app.config['WIKI_PATH'], os.path.join(app.config['WIKI_PATH'], '.git')]:
if not os.access(dir_, mode):
sys.exit('Read and write access to WIKI_PATH is required (%s)' % dir_)

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

@ -20,7 +20,7 @@ def cname_to_filename(cname):
:return: str -- Filename :return: str -- Filename
""" """
return cname.lower() + ".md" return cname + ".md"
def filename_to_cname(filename): def filename_to_cname(filename):
@ -62,25 +62,35 @@ class Wiki(HookMixin):
def __repr__(self): def __repr__(self):
return "Wiki: %s" % self.path return "Wiki: %s" % self.path
def revert_page(self, name, commit_sha, message, username): 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, email):
"""Revert page to passed commit sha1 """Revert page to passed commit sha1
:param name: Name of page to revert. :param name: Name of page to revert.
:param commit_sha: Commit Sha1 to revert to. :param commit_sha: Commit Sha1 to revert to.
:param message: Commit message. :param message: Commit message.
:param username: :param username: Committer name.
:param email: Committer email.
:return: Git commit sha1 :return: Git commit sha1
""" """
page = self.get_page(name, commit_sha) page = self.get_page(name, commit_sha)
if not page: if not page:
raise PageNotFound() raise PageNotFound('Commit not found')
if not message: if not message:
commit_info = gittle.utils.git.commit_info(self.gittle[commit_sha.encode('latin-1')]) commit_info = gittle.utils.git.commit_info(self.gittle[commit_sha.encode('latin-1')])
message = commit_info['message'] message = commit_info['message']
return self.write_page(name, page['data'], message=message, username=username) return self.write_page(name, page['data'], message=message, username=username, email=email)
def write_page(self, name, content, message=None, create=False, username=None, email=None): def write_page(self, name, content, message=None, create=False, username=None, email=None):
"""Write page to git repo """Write page to git repo
@ -108,11 +118,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 +174,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 +193,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,34 +1,72 @@
import json
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 cname_to_filename, filename_to_cname
from realms.modules.wiki.models import Wiki, cname_to_filename, filename_to_cname from realms.lib.test import BaseTest
from flask.ext.testing import TestCase
class WikiTest(TestCase): class WikiBaseTest(BaseTest):
def write_page(self, name, message=None, content=None):
return self.client.post(url_for('wiki.page_write', name=name),
data=dict(message=message, content=content))
def create_app(self):
app.config['TESTING'] = True
return app
def test_wiki_routes(self):
self.assert_200(self.client.get(url_for("wiki.create")))
""" Create a test page first!
for route in ['page', 'edit', 'history', 'index']:
rv = self.client.get(url_for("wiki.%s" % route, name='test'))
self.assert_200(rv, "wiki.%s: %s" % (route, rv.status_code))
"""
def test_write_page(self):
pass
def test_revert(self):
pass
class UtilTest(WikiBaseTest):
def test_cname_to_filename(self): def test_cname_to_filename(self):
eq_(cname_to_filename('test'), 'test.md') eq_(cname_to_filename('test'), 'test.md')
def test_filename_to_cname(self): def test_filename_to_cname(self):
eq_(filename_to_cname('test-1-2-3.md'), 'test-1-2-3') eq_(filename_to_cname('test-1-2-3.md'), 'test-1-2-3')
class WikiTest(WikiBaseTest):
def test_routes(self):
self.assert_200(self.client.get(url_for("wiki.create")))
self.write_page('test', message='test message', content='testing')
for route in ['page', 'edit', 'history']:
rv = self.client.get(url_for("wiki.%s" % route, name='test'))
self.assert_200(rv, "wiki.%s: %s" % (route, rv.status_code))
self.assert_200(self.client.get(url_for('wiki.index')))
def test_write_page(self):
self.assert_200(self.write_page('test', message='test message', content='testing'))
rv = self.client.get(url_for('wiki.page', name='test'))
self.assert_200(rv)
self.assert_context('name', 'test')
eq_(self.get_context_variable('page')['info']['message'], 'test message')
eq_(self.get_context_variable('page')['data'], 'testing')
def test_history(self):
self.assert_200(self.client.get(url_for('wiki.history', name='test')))
def test_delete_page(self):
self.app.config['WIKI_LOCKED_PAGES'] = ['test']
self.assert_403(self.client.delete(url_for('wiki.page_write', name='test')))
self.app.config['WIKI_LOCKED_PAGES'] = []
self.assert_200(self.client.delete(url_for('wiki.page_write', name='test')))
rv = self.client.get(url_for('wiki.page', name='test'))
self.assert_status(rv, 302)
def test_revert(self):
rv1 = self.write_page('test', message='test message', content='testing_old')
self.write_page('test', message='test message', content='testing_new')
data = json.loads(rv1.data)
self.client.post(url_for('wiki.revert'), data=dict(name='test', commit=data['sha']))
self.client.get(url_for('wiki.page', name='test'))
eq_(self.get_context_variable('page')['data'], 'testing_old')
self.assert_404(self.client.post(url_for('wiki.revert'), data=dict(name='test', commit='does not exist')))
self.app.config['WIKI_LOCKED_PAGES'] = ['test']
self.assert_403(self.client.post(url_for('wiki.revert'), data=dict(name='test', commit=data['sha'])))
self.app.config['WIKI_LOCKED_PAGES'] = []
class RelativePathTest(WikiTest):
def configure(self):
return dict(RELATIVE_PATH='wiki')

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 .models import PageNotFound
from realms import current_user, app
blueprint = Blueprint('wiki', __name__, url_prefix=app.config['RELATIVE_PATH']) blueprint = Blueprint('wiki', __name__)
@app.before_request
def init_wiki():
g.current_wiki = Wiki(app.config['WIKI_PATH'])
@blueprint.route("/_commit/<sha>/<name>") @blueprint.route("/_commit/<sha>/<name>")
@ -27,22 +21,29 @@ 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'])
@login_required @login_required
def revert(): def revert():
name = request.form.get('name') cname = to_canonical(request.form.get('name'))
commit = request.form.get('commit') commit = request.form.get('commit')
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"), 403
try:
sha = g.current_wiki.revert_page(cname,
commit,
message=message,
username=current_user.username,
email=current_user.email)
except PageNotFound as e:
return dict(error=True, message=e.message), 404
sha = g.current_wiki.revert_page(name, commit, message=message,
username=current_user.username)
if sha: if sha:
flash("Page reverted") flash("Page reverted")
@ -101,24 +102,25 @@ def page_write(name):
cname = to_canonical(name) cname = to_canonical(name)
if not cname: if not cname:
return dict(error=True, message="Invalid name") return dict(error=True, message="Invalid 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"), 403
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"), 403
if edit_cname != cname.lower(): if edit_cname != cname.lower():
g.current_wiki.rename_page(cname, edit_cname) g.current_wiki.rename_page(cname, edit_cname)
@ -126,13 +128,19 @@ 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) if cname in current_app.config.get('WIKI_LOCKED_PAGES'):
return dict(error=True, message="Page is locked"), 403
sha = g.current_wiki.delete_page(name,
username=current_user.username,
email=current_user.email)
return dict(sha=sha) return dict(sha=sha)

1
realms/version.py Normal file
View file

@ -0,0 +1 @@
__version__ = '0.3.24'

View file

@ -1,3 +1,4 @@
-r requirements.txt -r requirements.txt
Flask-Testing==0.4.2 Flask-Testing==0.4.2
nose==1.3.4 nose==1.3.4
blinker==1.3

View file

@ -1,18 +1 @@
Flask==0.10.1 -e .
Flask-Assets==0.10
Flask-Cache==0.13.1
Flask-Login==0.2.11
Flask-SQLAlchemy==2.0
Flask-WTF==0.10.2
PyYAML==3.11
bcrypt==1.0.2
beautifulsoup4==4.3.2
click==3.3
gevent==1.0.1
ghdiff==0.4
gittle==0.4.0
gunicorn==19.1.1
itsdangerous==0.24
lxml==3.4.0
markdown2==2.3.0
simplejson==3.6.3

View file

@ -12,8 +12,8 @@ with open('README.md') as f:
with open('requirements.txt') as f: with open('requirements.txt') as f:
required = f.read().splitlines() required = f.read().splitlines()
with open('VERSION') as f: __version__ = None
VERSION = f.read().strip() exec(open('realms/version.py').read())
CLASSIFIERS = [ CLASSIFIERS = [
'Intended Audience :: Developers', 'Intended Audience :: Developers',
@ -23,10 +23,28 @@ CLASSIFIERS = [
'Topic :: Internet :: WWW/HTTP :: Dynamic Content'] 'Topic :: Internet :: WWW/HTTP :: Dynamic Content']
setup(name='realms-wiki', setup(name='realms-wiki',
version=VERSION, version=__version__,
packages=find_packages(), packages=find_packages(),
install_requires=required, install_requires=[
#scripts=['realms-wiki'], 'Flask==0.10.1',
'Flask-Assets==0.10',
'Flask-Cache==0.13.1',
'Flask-Login==0.2.11',
'Flask-SQLAlchemy==2.0',
'Flask-WTF==0.10.2',
'PyYAML==3.11',
'bcrypt==1.0.2',
'beautifulsoup4==4.3.2',
'click==3.3',
'gevent==1.0.1',
'ghdiff==0.4',
'gittle==0.4.0',
'gunicorn==19.1.1',
'itsdangerous==0.24',
'lxml==3.4.0',
'markdown2==2.3.0',
'simplejson==3.6.3'
],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'realms-wiki = realms.cli:cli' 'realms-wiki = realms.cli:cli'