commit
						9de1e4bdd8
					
				
					 23 changed files with 354 additions and 216 deletions
				
			
		|  | @ -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 * | ||||
|  |  | |||
							
								
								
									
										1
									
								
								VERSION
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								VERSION
									
										
									
									
									
								
							|  | @ -1 +0,0 @@ | |||
| 0.3.20 | ||||
							
								
								
									
										10
									
								
								realms-wiki
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								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() | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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__': | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
							
								
								
									
										23
									
								
								realms/lib/test.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								realms/lib/test.py
									
										
									
									
									
										Normal 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:]]) | ||||
|  | @ -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,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_) | ||||
|  |  | |||
							
								
								
									
										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']) | ||||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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') | ||||
| 
 | ||||
| 
 | ||||
| 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') | ||||
|  |  | |||
|  | @ -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/<sha>/<name>") | ||||
|  | @ -27,22 +21,29 @@ 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']) | ||||
| @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) | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										1
									
								
								realms/version.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								realms/version.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| __version__ = '0.3.24' | ||||
|  | @ -1,3 +1,4 @@ | |||
| -r requirements.txt | ||||
| Flask-Testing==0.4.2 | ||||
| nose==1.3.4 | ||||
| blinker==1.3 | ||||
|  |  | |||
|  | @ -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 . | ||||
|  |  | |||
							
								
								
									
										28
									
								
								setup.py
									
										
									
									
									
								
							
							
						
						
									
										28
									
								
								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' | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue