This commit is contained in:
Matthew Scragg 2013-12-02 17:50:19 -06:00
parent d68c6f1d4e
commit 02b6b7d592
7 changed files with 273 additions and 301 deletions

9
app.py
View file

@ -1,9 +1,10 @@
from gevent import monkey, pywsgi from gevent import monkey, pywsgi
monkey.patch_all() monkey.patch_all()
from realms import config, init_db, make_app, SubdomainDispatcher import logging
from realms import app, config
if __name__ == '__main__': if __name__ == '__main__':
init_db(config.db['dbname']) print "Starting server"
app = SubdomainDispatcher(config.domain, make_app) app.logger.setLevel(logging.INFO)
pywsgi.WSGIServer(('', config.port), app).serve_forever() pywsgi.WSGIServer(('', config.PORT), app).serve_forever()

View file

@ -1,10 +1,9 @@
import logging import logging
import os import os
import time import time
from threading import Lock from tldextract import tldextract
import rethinkdb as rdb from flask import Flask, g, request, render_template, url_for, redirect, flash, session, current_app
from flask import Flask, g, request, render_template, url_for, redirect, flash, session
from flask.ctx import _AppCtxGlobals from flask.ctx import _AppCtxGlobals
from flask.ext.login import LoginManager, login_required from flask.ext.login import LoginManager, login_required
from flask.ext.assets import Environment, Bundle from flask.ext.assets import Environment, Bundle
@ -20,16 +19,14 @@ from realms.lib.services import db
from models import Site, User, CurrentUser from models import Site, User, CurrentUser
# Flask instance container
instances = {}
# Flask extension objects
login_manager = LoginManager()
assets = Environment()
class AppCtxGlobals(_AppCtxGlobals): class AppCtxGlobals(_AppCtxGlobals):
@cached_property
def current_site(self):
ext = tldextract.extract(request.host)
print ext
return ext.subdomain
@cached_property @cached_property
def current_user(self): def current_user(self):
return session.get('user') if session.get('user') else {'username': 'Anon'} return session.get('user') if session.get('user') else {'username': 'Anon'}
@ -38,54 +35,30 @@ class AppCtxGlobals(_AppCtxGlobals):
class Application(Flask): class Application(Flask):
app_ctx_globals_class = AppCtxGlobals app_ctx_globals_class = AppCtxGlobals
class SubdomainDispatcher(object):
"""
Application factory
"""
def __init__(self, domain, create_app):
self.domain = domain
self.create_app = create_app
self.lock = Lock()
def get_application(self, host):
host = host.split(':')[0]
assert host.endswith(self.domain), 'Configuration error'
subdomain = host[:-len(self.domain)].rstrip('.')
with self.lock:
app = instances.get(subdomain)
if app is None:
app = self.create_app(subdomain)
instances[subdomain] = app
return app
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
app = self.get_application(environ['HTTP_HOST']) path_info = environ.get('PATH_INFO')
return app(environ, start_response)
if path_info and len(path_info) > 1 and path_info.endswith('/'):
environ['PATH_INFO'] = path_info[:-1]
scheme = environ.get('HTTP_X_SCHEME')
if scheme:
environ['wsgi.url_scheme'] = scheme
real_ip = environ.get('HTTP_X_REAL_IP')
if real_ip:
environ['REMOTE_ADDR'] = real_ip
return super(Application, self).__call__(environ, start_response)
def init_db(dbname): def init_db(dbname):
""" """
Assures DB has minimal setup Assures DB has minimal setup
""" """
if not dbname in rdb.db_list().run(db): pass
print "Creating DB %s" % dbname
rdb.db_create(dbname).run(db)
for tbl in ['sites', 'users', 'pages']:
if not tbl in rdb.table_list().run(db):
rdb.table_create(tbl).run(db)
if not 'name' in rdb.table('sites').index_list().run(db):
rdb.table('sites').index_create('name').run(db)
for i in ['username', 'email']:
if not i in rdb.table('users').index_list().run(db):
rdb.table('users').index_create(i).run(db)
s = Site()
if not s.get_by_name('_'):
s.create(name='_', repo='_')
class RegexConverter(BaseConverter): class RegexConverter(BaseConverter):
@ -114,36 +87,32 @@ def format_subdomain(s):
return s return s
def make_app(subdomain): app = Application(__name__)
subdomain = format_subdomain(subdomain) app.config.update(config.FLASK)
if subdomain and not Wiki.is_registered(subdomain): app.debug = (config.ENV is not 'PROD')
return redirect("http://%s/_new/?site=%s" % (config.hostname, subdomain)) app.secret_key = config.SECRET_KEY
return create_app(subdomain) app.static_path = os.sep + 'static'
app.session_interface = RedisSessionInterface()
app.url_map.converters['regex'] = RegexConverter
# Flask extension objects
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
def create_app(subdomain=None): @login_manager.user_loader
app = Application(__name__) def load_user(user_id):
app.config.update(config.flask)
app.debug = (config.ENV is not 'PROD')
app.secret_key = config.secret_key
app.static_path = os.sep + 'static'
app.session_interface = RedisSessionInterface()
app.url_map.converters['regex'] = RegexConverter
login_manager.init_app(app)
login_manager.login_view = 'login'
@login_manager.user_loader
def load_user(user_id):
return CurrentUser(user_id) return CurrentUser(user_id)
assets.init_app(app) assets = Environment()
if config.ENV is 'PROD': assets.init_app(app)
if config.ENV is 'PROD':
if 'js_common' not in assets._named_bundles: if 'js_common' not in assets._named_bundles:
assets.register('js_common', Bundle('packed-common.js')) assets.register('js_common', Bundle('packed-common.js'))
if 'js_editor' not in assets._named_bundles: if 'js_editor' not in assets._named_bundles:
assets.register('js_editor', Bundle('packed-editor.js')) assets.register('js_editor', Bundle('packed-editor.js'))
else: else:
if 'js_common' not in assets._named_bundles: if 'js_common' not in assets._named_bundles:
js = Bundle( js = Bundle(
Bundle('vendor/jquery/jquery.js', Bundle('vendor/jquery/jquery.js',
@ -168,13 +137,11 @@ def create_app(subdomain=None):
filters='closure_js', output='packed-editor.js') filters='closure_js', output='packed-editor.js')
assets.register('js_editor', js) assets.register('js_editor', js)
repo_dir = config.repos['dir'] repo_dir = config.REPO_DIR
repo_name = subdomain if subdomain else "_"
w = Wiki(repo_dir + "/" + repo_name)
@app.after_request @app.after_request
def inject_x_rate_headers(response): def inject_x_rate_headers(response):
limit = get_view_rate_limit() limit = get_view_rate_limit()
if limit and limit.send_x_headers: if limit and limit.send_x_headers:
h = response.headers h = response.headers
@ -183,36 +150,43 @@ def create_app(subdomain=None):
h.add('X-RateLimit-Reset', str(limit.reset)) h.add('X-RateLimit-Reset', str(limit.reset))
return response return response
@app.template_filter('datetime')
def _jinja2_filter_datetime(ts): @app.template_filter('datetime')
def _jinja2_filter_datetime(ts):
return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts)) return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts))
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(e):
return render_template('errors/404.html'), 404 return render_template('errors/404.html'), 404
@app.errorhandler(500)
def page_error(e): @app.errorhandler(500)
def page_error(e):
logging.exception(e) logging.exception(e)
return render_template('errors/500.html'), 500 return render_template('errors/500.html'), 500
@app.route("/")
@ratelimiter(limit=50, per=60) @app.route("/")
def root(): @ratelimiter(limit=50, per=60)
def root():
return g.current_site
return render('home') return render('home')
@app.route("/home")
def home(): @app.route("/home")
def home():
return redirect(url_for('root')) return redirect(url_for('root'))
@app.route("/_account/")
@login_required @app.route("/_account/")
def account(): @login_required
def account():
return render_template('account/index.html') return render_template('account/index.html')
@app.route("/_new/", methods=['GET', 'POST'])
@login_required @app.route("/_new/", methods=['GET', 'POST'])
def new_wiki(): @login_required
def new_wiki():
if request.method == 'POST': if request.method == 'POST':
wiki_name = to_canonical(request.form['name']) wiki_name = to_canonical(request.form['name'])
@ -222,43 +196,47 @@ def create_app(subdomain=None):
else: else:
s = Site() s = Site()
s.create(name=wiki_name, repo=wiki_name, founder=g.current_user.get('id')) s.create(name=wiki_name, repo=wiki_name, founder=g.current_user.get('id'))
instances.pop(wiki_name, None)
return redirect('http://%s.%s' % (wiki_name, config.hostname)) return redirect('http://%s.%s' % (wiki_name, config.hostname))
else: else:
return render_template('_new/index.html') return render_template('_new/index.html')
@app.route("/_logout/")
def logout(): @app.route("/_logout/")
def logout():
User.logout() User.logout()
return redirect(url_for('root')) return redirect(url_for('root'))
@app.route("/_commit/<sha>/<name>")
def commit_sha(name, sha): @app.route("/_commit/<sha>/<name>")
def commit_sha(name, sha):
cname = to_canonical(name) cname = to_canonical(name)
data = w.get_page(cname, sha=sha) data = Wiki.get_page(cname, sha=sha)
if data: if data:
return render_template('page/page.html', name=name, page=data, commit=sha) return render_template('page/page.html', name=name, page=data, commit=sha)
else: else:
return redirect('/_create/'+cname) return redirect('/_create/'+cname)
@app.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
def compare(name, fsha, dots, lsha): @app.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
diff = w.compare(name, fsha, lsha) def compare(name, fsha, dots, lsha):
diff = Wiki.compare(name, fsha, lsha)
return render_template('page/compare.html', name=name, diff=diff, old=fsha, new=lsha) return render_template('page/compare.html', name=name, diff=diff, old=fsha, new=lsha)
@app.route("/_revert", methods=['POST'])
def revert(): @app.route("/_revert", methods=['POST'])
def revert():
if request.method == 'POST': if request.method == 'POST':
name = request.form.get('name') name = request.form.get('name')
commit = request.form.get('commit') commit = request.form.get('commit')
cname = to_canonical(name) cname = to_canonical(name)
w.revert_page(name, commit, message="Reverting %s" % cname, username=g.current_user.get('username')) Wiki.revert_page(name, commit, message="Reverting %s" % cname, username=g.current_user.get('username'))
flash('Page reverted', 'success') flash('Page reverted', 'success')
return redirect("/" + cname) return redirect("/" + cname)
@app.route("/_register", methods=['GET', 'POST'])
def register(): @app.route("/_register", methods=['GET', 'POST'])
def register():
if request.method == 'POST': if request.method == 'POST':
if User.register(request.form.get('username'), request.form.get('email'), request.form.get('password')): if User.register(request.form.get('username'), request.form.get('email'), request.form.get('password')):
return redirect(url_for('root')) return redirect(url_for('root'))
@ -268,8 +246,9 @@ def create_app(subdomain=None):
else: else:
return render_template('account/register.html') return render_template('account/register.html')
@app.route("/_login", methods=['GET', 'POST'])
def login(): @app.route("/_login", methods=['GET', 'POST'])
def login():
if request.method == 'POST': if request.method == 'POST':
if User.auth(request.form['email'], request.form['password']): if User.auth(request.form['email'], request.form['password']):
return redirect(redirect_url(referrer=url_for('root'))) return redirect(redirect_url(referrer=url_for('root')))
@ -279,20 +258,23 @@ def create_app(subdomain=None):
else: else:
return render_template('account/login.html') return render_template('account/login.html')
@app.route("/_history/<name>")
def history(name): @app.route("/_history/<name>")
history = w.get_history(name) def history(name):
history = Wiki.get_history(name)
return render_template('page/history.html', name=name, history=history) return render_template('page/history.html', name=name, history=history)
@app.route("/_edit/<name>", methods=['GET', 'POST'])
def edit(name): @app.route("/_edit/<name>", methods=['GET', 'POST'])
data = w.get_page(name) def edit(name):
data = Wiki.get_page(name)
cname = to_canonical(name) cname = to_canonical(name)
if request.method == 'POST': if request.method == 'POST':
edit_cname = to_canonical(request.form['name']) edit_cname = to_canonical(request.form['name'])
if edit_cname.lower() != cname.lower(): if edit_cname.lower() != cname.lower():
w.rename_page(cname, edit_cname) Wiki.rename_page(cname, edit_cname)
w.write_page(edit_cname, request.form['content'], Wiki.write_page(edit_cname,
request.form['content'],
message=request.form['message'], message=request.form['message'],
username=g.current_user.get('username')) username=g.current_user.get('username'))
return redirect("/" + edit_cname) return redirect("/" + edit_cname)
@ -304,22 +286,25 @@ def create_app(subdomain=None):
else: else:
return redirect('/_create/'+cname) return redirect('/_create/'+cname)
@app.route("/_delete/<name>", methods=['POST'])
@login_required @app.route("/_delete/<name>", methods=['POST'])
def delete(name): @login_required
def delete(name):
pass pass
@app.route("/_create/", methods=['GET', 'POST'])
@app.route("/_create/<name>", methods=['GET', 'POST']) @app.route("/_create/", methods=['GET', 'POST'])
def create(name=None): @app.route("/_create/<name>", methods=['GET', 'POST'])
def create(name=None):
cname = "" cname = ""
if name: if name:
cname = to_canonical(name) cname = to_canonical(name)
if w.get_page(cname): if Wiki.get_page(cname):
# Page exists, edit instead # Page exists, edit instead
return redirect("/edit/" + cname) return redirect("/edit/" + cname)
if request.method == 'POST': if request.method == 'POST':
w.write_page(request.form['name'], request.form['content'], Wiki.write_page(request.form['name'],
request.form['content'],
message=request.form['message'], message=request.form['message'],
create=True, create=True,
username=g.current_user.get('username')) username=g.current_user.get('username'))
@ -327,16 +312,15 @@ def create_app(subdomain=None):
else: else:
return render_template('page/edit.html', name=cname, content="") return render_template('page/edit.html', name=cname, content="")
@app.route("/<name>")
def render(name): @app.route("/<name>")
def render(name):
cname = to_canonical(name) cname = to_canonical(name)
if cname != name: if cname != name:
return redirect('/' + cname) return redirect('/' + cname)
data = w.get_page(cname) data = Wiki.get_page(cname)
if data: if data:
return render_template('page/page.html', name=cname, page=data) return render_template('page/page.html', name=cname, page=data)
else: else:
return redirect('/_create/'+cname) return redirect('/_create/'+cname)
return app

View file

@ -1,10 +1,9 @@
import rethinkdb as rdb
import redis import redis
from realms import config from realms import config
from sqlalchemy import create_engine
# Default DB connection # Default DB connection
db = rdb.connect(config.db['host'], config.db['port'], db=config.db['dbname']) db = create_engine(config.DB_URI, encoding='utf8', echo=True)
# Default Cache connection # Default Cache connection
cache = redis.StrictRedis(host=config.cache['host'], port=config.cache['port']) cache = redis.StrictRedis(host=config.REDIS_HOST, port=config.REDIS_PORT)

View file

@ -1,15 +1,13 @@
import os import os
import re import re
import lxml.html import lxml.html
from lxml.html import clean
import ghdiff import ghdiff
import gittle.utils import gittle.utils
from gittle import Gittle from gittle import Gittle
from dulwich.repo import NotGitRepository from dulwich.repo import NotGitRepository
from werkzeug.utils import escape, unescape from werkzeug.utils import escape, unescape
from util import to_canonical from util import to_canonical
from models import Site from realms.models import Site
class MyGittle(Gittle): class MyGittle(Gittle):
@ -94,7 +92,7 @@ class Wiki():
tree = lxml.html.fromstring(content) tree = lxml.html.fromstring(content)
cleaner = clean.Cleaner(remove_unknown_tags=False, kill_tags=set(['style']), safe_attrs_only=False) cleaner = lxml.html.Cleaner(remove_unknown_tags=False, kill_tags=set(['style']), safe_attrs_only=False)
tree = cleaner.clean_html(tree) tree = cleaner.clean_html(tree)
content = lxml.html.tostring(tree, encoding='utf-8', method='html') content = lxml.html.tostring(tree, encoding='utf-8', method='html')

View file

@ -1,40 +1,12 @@
import rethinkdb as rdb
import bcrypt import bcrypt
from sqlalchemy import Column, Integer, String, Time
from sqlalchemy.ext.declarative import declarative_base
from flask import session, flash from flask import session, flash
from flask.ext.login import login_user, logout_user from flask.ext.login import login_user, logout_user
from realms.lib.util import gravatar_url, to_dict, cache_it from realms.lib.util import gravatar_url, to_dict
from realms.lib.services import db from realms.lib.services import db
Base = declarative_base()
class BaseModel():
table = None
_conn = db
def __init__(self, **kwargs):
if not kwargs.get('conn'):
kwargs['conn'] = db
def create(self, **kwargs):
return rdb.table(self.table).insert(kwargs).run(self._conn)
@cache_it
def get_by_id(self, id):
return rdb.table(self.table).get(id).run(self._conn)
def get_all(self, arg, index):
return rdb.table(self.table).get_all(arg, index=index).run(self._conn)
#@cache_it
def get_one(self, arg, index):
return rdb.table(self.table).get_all(arg, index=index).limit(1).run(self._conn)
class Site(BaseModel):
table = 'sites'
def get_by_name(self, name):
return to_dict(self.get_one(name, 'name'), True)
class CurrentUser(): class CurrentUser():
@ -66,8 +38,22 @@ class CurrentUser():
return None return None
class User(BaseModel): class Site(Base):
table = 'users' __tablename__ = 'sites'
id = Column(Integer, primary_key=True)
name = Column(String(100))
pages = Column(Integer)
views = Column(Integer)
created = Column(Time)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(100))
email = Column(String(255))
password = Column(String(255))
joined = Column(Time)
def get_by_email(self, email): def get_by_email(self, email):
return to_dict(self.get_one(email, 'email'), True) return to_dict(self.get_one(email, 'email'), True)

View file

@ -3,21 +3,23 @@ Flask-Assets==0.8
Flask-Bcrypt==0.5.2 Flask-Bcrypt==0.5.2
Flask-Login==0.2.7 Flask-Login==0.2.7
beautifulsoup4==4.3.2 beautifulsoup4==4.3.2
boto==2.13.3 boto==2.17.0
closure==20121212 closure==20121212
gevent==0.13.8 gevent==0.13.8
ghdiff==0.1 ghdiff==0.1
gittle==0.2.2 gittle==0.2.2
itsdangerous==0.23 itsdangerous==0.23
lxml==3.2.3 lxml==3.2.4
markdown2==2.1.0 markdown2==2.1.0
pyzmq==13.0.0 pyzmq==14.0.0
recaptcha==1.0rc1 recaptcha==1.0rc1
recaptcha-client==1.0.6 recaptcha-client==1.0.6
redis==2.8.0 redis==2.8.0
rethinkdb==1.10.0-0 rethinkdb==1.10.0-0
simplejson==3.3.0 simplejson==3.3.1
sockjs-tornado==1.0.0 sockjs-tornado==1.0.0
supervisor==3.0 supervisor==3.0
SQLAlchemy==0.8.3 SQLAlchemy==0.8.3
tornado==3.1.1 tornado==3.1.1
tldextract==1.2.2
psycopg2==2.5.1

View file

@ -2,6 +2,8 @@ postgresql:
pkg.installed: pkg.installed:
- name: postgresql-9.3 - name: postgresql-9.3
libpq-dev:
pkg.installed
createdb: createdb:
cmd.run: cmd.run: