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,229 +87,240 @@ 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) return CurrentUser(user_id)
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) assets = Environment()
login_manager.login_view = 'login' assets.init_app(app)
if config.ENV is 'PROD':
if 'js_common' not in assets._named_bundles:
assets.register('js_common', Bundle('packed-common.js'))
if 'js_editor' not in assets._named_bundles:
assets.register('js_editor', Bundle('packed-editor.js'))
else:
if 'js_common' not in assets._named_bundles:
js = Bundle(
Bundle('vendor/jquery/jquery.js',
'vendor/components-underscore/underscore.js',
'vendor/components-bootstrap/js/bootstrap.js',
'vendor/handlebars/handlebars.js',
'vendor/showdown/src/showdown.js',
'vendor/showdown/src/extensions/table.js',
'js/wmd.js',
filters='closure_js'),
'js/html-sanitizer-minified.js',
'vendor/highlightjs/highlight.pack.js',
Bundle('js/main.js', filters='closure_js'),
output='packed-common.js')
assets.register('js_common', js)
@login_manager.user_loader if 'js_editor' not in assets._named_bundles:
def load_user(user_id): js = Bundle('js/ace/ace.js',
return CurrentUser(user_id) 'js/ace/mode-markdown.js',
'vendor/keymaster/keymaster.js',
'js/dillinger.js',
filters='closure_js', output='packed-editor.js')
assets.register('js_editor', js)
assets.init_app(app) repo_dir = config.REPO_DIR
if config.ENV is 'PROD':
if 'js_common' not in assets._named_bundles:
assets.register('js_common', Bundle('packed-common.js')) @app.after_request
if 'js_editor' not in assets._named_bundles: def inject_x_rate_headers(response):
assets.register('js_editor', Bundle('packed-editor.js')) limit = get_view_rate_limit()
if limit and limit.send_x_headers:
h = response.headers
h.add('X-RateLimit-Remaining', str(limit.remaining))
h.add('X-RateLimit-Limit', str(limit.limit))
h.add('X-RateLimit-Reset', str(limit.reset))
return response
@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.errorhandler(500)
def page_error(e):
logging.exception(e)
return render_template('errors/500.html'), 500
@app.route("/")
@ratelimiter(limit=50, per=60)
def root():
return g.current_site
return render('home')
@app.route("/home")
def home():
return redirect(url_for('root'))
@app.route("/_account/")
@login_required
def account():
return render_template('account/index.html')
@app.route("/_new/", methods=['GET', 'POST'])
@login_required
def new_wiki():
if request.method == 'POST':
wiki_name = to_canonical(request.form['name'])
if Wiki.is_registered(wiki_name):
flash("Site already exists")
return redirect(redirect_url())
else:
s = Site()
s.create(name=wiki_name, repo=wiki_name, founder=g.current_user.get('id'))
return redirect('http://%s.%s' % (wiki_name, config.hostname))
else: else:
if 'js_common' not in assets._named_bundles: return render_template('_new/index.html')
js = Bundle(
Bundle('vendor/jquery/jquery.js',
'vendor/components-underscore/underscore.js',
'vendor/components-bootstrap/js/bootstrap.js',
'vendor/handlebars/handlebars.js',
'vendor/showdown/src/showdown.js',
'vendor/showdown/src/extensions/table.js',
'js/wmd.js',
filters='closure_js'),
'js/html-sanitizer-minified.js',
'vendor/highlightjs/highlight.pack.js',
Bundle('js/main.js', filters='closure_js'),
output='packed-common.js')
assets.register('js_common', js)
if 'js_editor' not in assets._named_bundles:
js = Bundle('js/ace/ace.js',
'js/ace/mode-markdown.js',
'vendor/keymaster/keymaster.js',
'js/dillinger.js',
filters='closure_js', output='packed-editor.js')
assets.register('js_editor', js)
repo_dir = config.repos['dir'] @app.route("/_logout/")
repo_name = subdomain if subdomain else "_" def logout():
User.logout()
return redirect(url_for('root'))
w = Wiki(repo_dir + "/" + repo_name)
@app.after_request @app.route("/_commit/<sha>/<name>")
def inject_x_rate_headers(response): def commit_sha(name, sha):
limit = get_view_rate_limit() cname = to_canonical(name)
if limit and limit.send_x_headers:
h = response.headers
h.add('X-RateLimit-Remaining', str(limit.remaining))
h.add('X-RateLimit-Limit', str(limit.limit))
h.add('X-RateLimit-Reset', str(limit.reset))
return response
@app.template_filter('datetime') data = Wiki.get_page(cname, sha=sha)
def _jinja2_filter_datetime(ts): if data:
return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts)) return render_template('page/page.html', name=name, page=data, commit=sha)
else:
return redirect('/_create/'+cname)
@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404
@app.errorhandler(500) @app.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
def page_error(e): def compare(name, fsha, dots, lsha):
logging.exception(e) diff = Wiki.compare(name, fsha, lsha)
return render_template('errors/500.html'), 500 return render_template('page/compare.html', name=name, diff=diff, old=fsha, new=lsha)
@app.route("/")
@ratelimiter(limit=50, per=60)
def root():
return render('home')
@app.route("/home") @app.route("/_revert", methods=['POST'])
def home(): def revert():
return redirect(url_for('root')) if request.method == 'POST':
name = request.form.get('name')
@app.route("/_account/") commit = request.form.get('commit')
@login_required
def account():
return render_template('account/index.html')
@app.route("/_new/", methods=['GET', 'POST'])
@login_required
def new_wiki():
if request.method == 'POST':
wiki_name = to_canonical(request.form['name'])
if Wiki.is_registered(wiki_name):
flash("Site already exists")
return redirect(redirect_url())
else:
s = Site()
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))
else:
return render_template('_new/index.html')
@app.route("/_logout/")
def logout():
User.logout()
return redirect(url_for('root'))
@app.route("/_commit/<sha>/<name>")
def commit_sha(name, sha):
cname = to_canonical(name) cname = to_canonical(name)
Wiki.revert_page(name, commit, message="Reverting %s" % cname, username=g.current_user.get('username'))
flash('Page reverted', 'success')
return redirect("/" + cname)
data = w.get_page(cname, sha=sha)
@app.route("/_register", methods=['GET', 'POST'])
def register():
if request.method == 'POST':
if User.register(request.form.get('username'), request.form.get('email'), request.form.get('password')):
return redirect(url_for('root'))
else:
# Login failed
return redirect(url_for('register'))
else:
return render_template('account/register.html')
@app.route("/_login", methods=['GET', 'POST'])
def login():
if request.method == 'POST':
if User.auth(request.form['email'], request.form['password']):
return redirect(redirect_url(referrer=url_for('root')))
else:
flash("Email or Password invalid")
return redirect("/_login")
else:
return render_template('account/login.html')
@app.route("/_history/<name>")
def history(name):
history = Wiki.get_history(name)
return render_template('page/history.html', name=name, history=history)
@app.route("/_edit/<name>", methods=['GET', 'POST'])
def edit(name):
data = Wiki.get_page(name)
cname = to_canonical(name)
if request.method == 'POST':
edit_cname = to_canonical(request.form['name'])
if edit_cname.lower() != cname.lower():
Wiki.rename_page(cname, edit_cname)
Wiki.write_page(edit_cname,
request.form['content'],
message=request.form['message'],
username=g.current_user.get('username'))
return redirect("/" + edit_cname)
else:
if data: if data:
return render_template('page/page.html', name=name, page=data, commit=sha) name = remove_ext(data['name'])
content = data['data']
return render_template('page/edit.html', name=name, content=content)
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):
diff = w.compare(name, fsha, lsha)
return render_template('page/compare.html', name=name, diff=diff, old=fsha, new=lsha)
@app.route("/_revert", methods=['POST']) @app.route("/_delete/<name>", methods=['POST'])
def revert(): @login_required
if request.method == 'POST': def delete(name):
name = request.form.get('name') pass
commit = request.form.get('commit')
cname = to_canonical(name)
w.revert_page(name, commit, message="Reverting %s" % cname, username=g.current_user.get('username'))
flash('Page reverted', 'success')
return redirect("/" + cname)
@app.route("/_register", methods=['GET', 'POST'])
def register():
if request.method == 'POST':
if User.register(request.form.get('username'), request.form.get('email'), request.form.get('password')):
return redirect(url_for('root'))
else:
# Login failed
return redirect(url_for('register'))
else:
return render_template('account/register.html')
@app.route("/_login", methods=['GET', 'POST']) @app.route("/_create/", methods=['GET', 'POST'])
def login(): @app.route("/_create/<name>", methods=['GET', 'POST'])
if request.method == 'POST': def create(name=None):
if User.auth(request.form['email'], request.form['password']): cname = ""
return redirect(redirect_url(referrer=url_for('root'))) if name:
else:
flash("Email or Password invalid")
return redirect("/_login")
else:
return render_template('account/login.html')
@app.route("/_history/<name>")
def history(name):
history = w.get_history(name)
return render_template('page/history.html', name=name, history=history)
@app.route("/_edit/<name>", methods=['GET', 'POST'])
def edit(name):
data = w.get_page(name)
cname = to_canonical(name) cname = to_canonical(name)
if request.method == 'POST': if Wiki.get_page(cname):
edit_cname = to_canonical(request.form['name']) # Page exists, edit instead
if edit_cname.lower() != cname.lower(): return redirect("/edit/" + cname)
w.rename_page(cname, edit_cname) if request.method == 'POST':
w.write_page(edit_cname, request.form['content'], Wiki.write_page(request.form['name'],
message=request.form['message'], request.form['content'],
username=g.current_user.get('username')) message=request.form['message'],
return redirect("/" + edit_cname) create=True,
else: username=g.current_user.get('username'))
if data: return redirect("/" + cname)
name = remove_ext(data['name']) else:
content = data['data'] return render_template('page/edit.html', name=cname, content="")
return render_template('page/edit.html', name=name, content=content)
else:
return redirect('/_create/'+cname)
@app.route("/_delete/<name>", methods=['POST'])
@login_required
def delete(name):
pass
@app.route("/_create/", methods=['GET', 'POST']) @app.route("/<name>")
@app.route("/_create/<name>", methods=['GET', 'POST']) def render(name):
def create(name=None): cname = to_canonical(name)
cname = "" if cname != name:
if name: return redirect('/' + cname)
cname = to_canonical(name)
if w.get_page(cname):
# Page exists, edit instead
return redirect("/edit/" + cname)
if request.method == 'POST':
w.write_page(request.form['name'], request.form['content'],
message=request.form['message'],
create=True,
username=g.current_user.get('username'))
return redirect("/" + cname)
else:
return render_template('page/edit.html', name=cname, content="")
@app.route("/<name>") data = Wiki.get_page(cname)
def render(name): if data:
cname = to_canonical(name) return render_template('page/page.html', name=cname, page=data)
if cname != name: else:
return redirect('/' + cname) return redirect('/_create/'+cname)
data = w.get_page(cname)
if data:
return render_template('page/page.html', name=cname, page=data)
else:
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: