diff --git a/MANIFEST.in b/MANIFEST.in index d72fe63..a5cc0d6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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/css * recursive-include realms/static/js * diff --git a/VERSION b/VERSION deleted file mode 100644 index 771f209..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.3.20 \ No newline at end of file diff --git a/realms-wiki b/realms-wiki index 7f797a5..1baa18f 100755 --- a/realms-wiki +++ b/realms-wiki @@ -1,6 +1,14 @@ #!/usr/bin/env python - +import pip from realms.cli import cli 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() diff --git a/realms/__init__.py b/realms/__init__.py index a5c0481..6bfa461 100644 --- a/realms/__init__.py +++ b/realms/__init__.py @@ -18,9 +18,10 @@ from flask.ext.assets import Environment, Bundle from werkzeug.routing import BaseConverter from werkzeug.exceptions import HTTPException -from realms.lib.util import to_canonical, remove_ext, mkdir_safe, gravatar_url, to_dict -from realms.lib.hook import HookModelMeta -from realms.lib.util import is_su, in_virtualenv +from .lib.util import to_canonical, remove_ext, mkdir_safe, gravatar_url, to_dict +from .lib.hook import HookModelMeta +from .lib.util import is_su, in_virtualenv +from .version import __version__ class Application(Flask): @@ -60,15 +61,26 @@ 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) - # 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): if rv is None: @@ -148,56 +160,55 @@ 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 -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) + 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.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 -@app.template_filter('datetime') -def _jinja2_filter_datetime(ts): - return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts)) + if app.config['RELATIVE_PATH']: + @app.route("/") + def root(): + return redirect(url_for(app.config['ROOT_ENDPOINT'])) + app.discover() -@app.errorhandler(404) -def page_not_found(e): - return render_template('errors/404.html'), 404 + # This will be removed at some point + with app.app_context(): + db.create_all() -if app.config['RELATIVE_PATH']: - @app.route("/") - def root(): - return redirect(url_for(app.config['ROOT_ENDPOINT'])) + return app -@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 -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,13 +231,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() - - - - - +@click.group() +def cli(): + pass diff --git a/realms/cli.py b/realms/cli.py index be4237a..4645fe6 100644 --- a/realms/cli.py +++ b/realms/cli.py @@ -1,11 +1,40 @@ -from realms import config, app, db, cli -from realms.lib.util import random_string, in_virtualenv +from realms import config, create_app, db, cli as cli_, __version__ +from realms.lib.util import is_su, random_string, in_virtualenv, green, yellow, red from subprocess import call, Popen from multiprocessing import cpu_count import click import json import sys 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(): @@ -33,18 +62,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, @@ -126,37 +143,28 @@ def get_prefix(): return sys.prefix -def get_pip(): - """ Get virtualenv path for pip - """ - if in_virtualenv(): - return get_prefix() + '/bin/pip' - else: - return 'pip' - - -@cli.command() +@cli.command(name='pip') @click.argument('cmd', nargs=-1) -def pip(cmd): +def pip_(cmd): """ Execute pip commands, useful for virtualenvs """ - call(get_pip() + ' ' + ' '.join(cmd), shell=True) + pip.main(cmd) def install_redis(): - call([get_pip(), 'install', 'redis']) + pip.main(['install', 'redis']) def install_mysql(): - call([get_pip(), 'install', 'MySQL-Python']) + pip.main(['install', 'MySQL-Python']) def install_postgres(): - call([get_pip(), 'install', 'psycopg2']) + pip.main(['install', 'psycopg2']) def install_memcached(): - call([get_pip(), 'install', 'python-memcached']) + pip.main(['install', 'python-memcached']) @click.command() @@ -229,9 +237,9 @@ def dev(port): """ Run development server """ green("Starting development server") - app.run(host="0.0.0.0", - port=port, - debug=True) + create_app().run(host="0.0.0.0", + port=port, + debug=True) def start_server(): @@ -243,8 +251,8 @@ def start_server(): green("Server started. Port: %s" % config.PORT) - Popen('gunicorn realms:app -b 0.0.0.0:%s -k gevent %s' % - (config.PORT, flags), shell=True, executable='/bin/bash') + Popen("gunicorn 'realms:create_app()' -b 0.0.0.0:%s -k gevent %s" % + (config.PORT, flags), shell=True, executable='/bin/bash') def stop_server(): @@ -317,21 +325,20 @@ def drop_db(): def test(): """ 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]): - call([get_pip(), 'install', mod[1]]) + pip.main(['install', mod[1]]) nosetests = get_prefix() + "/bin/nosetests" if in_virtualenv() else "nosetests" - call([nosetests, config.APP_PATH]) + call([nosetests, 'realms']) @cli.command() def version(): """ Output version """ - with open(os.path.join(config.APP_PATH, 'VERSION')) as f: - click.echo(f.read().strip()) + green(__version__) if __name__ == '__main__': diff --git a/realms/lib/hook.py b/realms/lib/hook.py index e4b2795..e772a1f 100644 --- a/realms/lib/hook.py +++ b/realms/lib/hook.py @@ -42,7 +42,6 @@ class HookMixin(object): return f return outer - @classmethod def before(cls, method_name): def outer(f, *args, **kwargs): diff --git a/realms/lib/model.py b/realms/lib/model.py index ecdd3a4..ac3d5cb 100644 --- a/realms/lib/model.py +++ b/realms/lib/model.py @@ -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): diff --git a/realms/lib/test.py b/realms/lib/test.py new file mode 100644 index 0000000..9a0f230 --- /dev/null +++ b/realms/lib/test.py @@ -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:]]) \ No newline at end of file diff --git a/realms/lib/util.py b/realms/lib/util.py index 8166ffa..cccf7b3 100644 --- a/realms/lib/util.py +++ b/realms/lib/util.py @@ -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 diff --git a/realms/modules/auth/commands.py b/realms/modules/auth/commands.py index 2e9c97a..ee101be 100644 --- a/realms/modules/auth/commands.py +++ b/realms/modules/auth/commands.py @@ -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) diff --git a/realms/modules/auth/forms.py b/realms/modules/auth/forms.py index da2db3e..bca2163 100644 --- a/realms/modules/auth/forms.py +++ b/realms/modules/auth/forms.py @@ -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()]) diff --git a/realms/modules/auth/hooks.py b/realms/modules/auth/hooks.py new file mode 100644 index 0000000..cbe1762 --- /dev/null +++ b/realms/modules/auth/hooks.py @@ -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?")) diff --git a/realms/modules/auth/models.py b/realms/modules/auth/models.py index ca1ab62..16be5c3 100644 --- a/realms/modules/auth/models.py +++ b/realms/modules/auth/models.py @@ -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): diff --git a/realms/modules/auth/views.py b/realms/modules/auth/views.py index e4d2297..70ba86a 100644 --- a/realms/modules/auth/views.py +++ b/realms/modules/auth/views.py @@ -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) diff --git a/realms/modules/wiki/__init__.py b/realms/modules/wiki/__init__.py index 2c61d69..c9ec232 100644 --- a/realms/modules/wiki/__init__.py +++ b/realms/modules/wiki/__init__.py @@ -1,13 +1,14 @@ import os import sys -from realms import app from realms.modules.wiki.models import Wiki -# Init Wiki -Wiki(app.config['WIKI_PATH']) -# 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_) +def init(app): + # Init Wiki + Wiki(app.config['WIKI_PATH']) + + # 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_) diff --git a/realms/modules/wiki/hooks.py b/realms/modules/wiki/hooks.py new file mode 100644 index 0000000..1d60723 --- /dev/null +++ b/realms/modules/wiki/hooks.py @@ -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']) \ No newline at end of file diff --git a/realms/modules/wiki/models.py b/realms/modules/wiki/models.py index 5d6b3fa..dc7d515 100644 --- a/realms/modules/wiki/models.py +++ b/realms/modules/wiki/models.py @@ -20,7 +20,7 @@ def cname_to_filename(cname): :return: str -- Filename """ - return cname.lower() + ".md" + return cname + ".md" def filename_to_cname(filename): @@ -62,25 +62,35 @@ class Wiki(HookMixin): def __repr__(self): 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 :param name: Name of page to revert. :param commit_sha: Commit Sha1 to revert to. :param message: Commit message. - :param username: + :param username: Committer name. + :param email: Committer email. :return: Git commit sha1 """ page = self.get_page(name, commit_sha) if not page: - raise PageNotFound() + raise PageNotFound('Commit not found') if not message: commit_info = gittle.utils.git.commit_info(self.gittle[commit_sha.encode('latin-1')]) 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): """Write page to git repo @@ -108,11 +118,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 +174,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 +193,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 diff --git a/realms/modules/wiki/tests.py b/realms/modules/wiki/tests.py index 4a95d16..daaa723 100644 --- a/realms/modules/wiki/tests.py +++ b/realms/modules/wiki/tests.py @@ -1,34 +1,72 @@ +import json 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.modules.wiki.models import cname_to_filename, filename_to_cname +from realms.lib.test import BaseTest -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): eq_(cname_to_filename('test'), 'test.md') def test_filename_to_cname(self): - eq_(filename_to_cname('test-1-2-3.md'), 'test-1-2-3') \ No newline at end of file + 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') diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py index 84e0078..d6f0329 100644 --- a/realms/modules/wiki/views.py +++ b/realms/modules/wiki/views.py @@ -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 +from .models import PageNotFound -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//") @@ -27,22 +21,29 @@ def commit(name, sha): @blueprint.route("/_compare//") 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']) @login_required def revert(): - name = request.form.get('name') + cname = to_canonical(request.form.get('name')) commit = request.form.get('commit') - cname = to_canonical(name) message = request.form.get('message', "Reverting %s" % cname) - if cname in app.config.get('WIKI_LOCKED_PAGES'): - return dict(error=True, message="Page is locked") + if cname in current_app.config.get('WIKI_LOCKED_PAGES'): + 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: flash("Page reverted") @@ -101,24 +102,25 @@ def page_write(name): cname = to_canonical(name) if not cname: - return dict(error=True, message="Invalid name") + return dict(error=True, message="Invalid name") if request.method == 'POST': # Create - if cname in app.config.get('WIKI_LOCKED_PAGES'): - return dict(error=True, message="Page is locked") + if cname in current_app.config.get('WIKI_LOCKED_PAGES'): + return dict(error=True, message="Page is locked"), 403 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'): - return dict(error=True, message="Page is locked") + if edit_cname in current_app.config.get('WIKI_LOCKED_PAGES'): + return dict(error=True, message="Page is locked"), 403 if edit_cname != cname.lower(): g.current_wiki.rename_page(cname, edit_cname) @@ -126,13 +128,19 @@ 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) + 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) diff --git a/realms/version.py b/realms/version.py new file mode 100644 index 0000000..7b5c7a3 --- /dev/null +++ b/realms/version.py @@ -0,0 +1 @@ +__version__ = '0.3.24' \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index d129051..041842c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ -r requirements.txt Flask-Testing==0.4.2 nose==1.3.4 +blinker==1.3 diff --git a/requirements.txt b/requirements.txt index 473dcb3..d6e1198 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1 @@ -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 +-e . diff --git a/setup.py b/setup.py index 55ad062..f5c332f 100644 --- a/setup.py +++ b/setup.py @@ -12,8 +12,8 @@ with open('README.md') as f: with open('requirements.txt') as f: required = f.read().splitlines() -with open('VERSION') as f: - VERSION = f.read().strip() +__version__ = None +exec(open('realms/version.py').read()) CLASSIFIERS = [ 'Intended Audience :: Developers', @@ -23,10 +23,28 @@ CLASSIFIERS = [ 'Topic :: Internet :: WWW/HTTP :: Dynamic Content'] setup(name='realms-wiki', - version=VERSION, + version=__version__, packages=find_packages(), - install_requires=required, - #scripts=['realms-wiki'], + install_requires=[ + '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={ 'console_scripts': [ 'realms-wiki = realms.cli:cli'