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
monkey.patch_all()
from realms import config, init_db, make_app, SubdomainDispatcher
import logging
from realms import app, config
if __name__ == '__main__':
init_db(config.db['dbname'])
app = SubdomainDispatcher(config.domain, make_app)
pywsgi.WSGIServer(('', config.port), app).serve_forever()
print "Starting server"
app.logger.setLevel(logging.INFO)
pywsgi.WSGIServer(('', config.PORT), app).serve_forever()

View file

@ -1,10 +1,9 @@
import logging
import os
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
from flask import Flask, g, request, render_template, url_for, redirect, flash, session, current_app
from flask.ctx import _AppCtxGlobals
from flask.ext.login import LoginManager, login_required
from flask.ext.assets import Environment, Bundle
@ -20,16 +19,14 @@ from realms.lib.services import db
from models import Site, User, CurrentUser
# Flask instance container
instances = {}
# Flask extension objects
login_manager = LoginManager()
assets = Environment()
class AppCtxGlobals(_AppCtxGlobals):
@cached_property
def current_site(self):
ext = tldextract.extract(request.host)
print ext
return ext.subdomain
@cached_property
def current_user(self):
return session.get('user') if session.get('user') else {'username': 'Anon'}
@ -38,54 +35,30 @@ class AppCtxGlobals(_AppCtxGlobals):
class Application(Flask):
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):
app = self.get_application(environ['HTTP_HOST'])
return app(environ, start_response)
path_info = environ.get('PATH_INFO')
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):
"""
Assures DB has minimal setup
"""
if not dbname in rdb.db_list().run(db):
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='_')
pass
class RegexConverter(BaseConverter):
@ -114,36 +87,32 @@ def format_subdomain(s):
return s
def make_app(subdomain):
subdomain = format_subdomain(subdomain)
if subdomain and not Wiki.is_registered(subdomain):
return redirect("http://%s/_new/?site=%s" % (config.hostname, subdomain))
return create_app(subdomain)
app = Application(__name__)
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
# Flask extension objects
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
def create_app(subdomain=None):
app = Application(__name__)
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):
@login_manager.user_loader
def load_user(user_id):
return CurrentUser(user_id)
assets.init_app(app)
if config.ENV is 'PROD':
assets = Environment()
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:
else:
if 'js_common' not in assets._named_bundles:
js = Bundle(
Bundle('vendor/jquery/jquery.js',
@ -168,13 +137,11 @@ def create_app(subdomain=None):
filters='closure_js', output='packed-editor.js')
assets.register('js_editor', js)
repo_dir = config.repos['dir']
repo_name = subdomain if subdomain else "_"
repo_dir = config.REPO_DIR
w = Wiki(repo_dir + "/" + repo_name)
@app.after_request
def inject_x_rate_headers(response):
@app.after_request
def inject_x_rate_headers(response):
limit = get_view_rate_limit()
if limit and limit.send_x_headers:
h = response.headers
@ -183,36 +150,43 @@ def create_app(subdomain=None):
h.add('X-RateLimit-Reset', str(limit.reset))
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))
@app.errorhandler(404)
def page_not_found(e):
@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def page_error(e):
@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():
@app.route("/")
@ratelimiter(limit=50, per=60)
def root():
return g.current_site
return render('home')
@app.route("/home")
def home():
@app.route("/home")
def home():
return redirect(url_for('root'))
@app.route("/_account/")
@login_required
def account():
@app.route("/_account/")
@login_required
def account():
return render_template('account/index.html')
@app.route("/_new/", methods=['GET', 'POST'])
@login_required
def new_wiki():
@app.route("/_new/", methods=['GET', 'POST'])
@login_required
def new_wiki():
if request.method == 'POST':
wiki_name = to_canonical(request.form['name'])
@ -222,43 +196,47 @@ def create_app(subdomain=None):
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():
@app.route("/_logout/")
def logout():
User.logout()
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)
data = w.get_page(cname, sha=sha)
data = Wiki.get_page(cname, sha=sha)
if data:
return render_template('page/page.html', name=name, page=data, commit=sha)
else:
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)
@app.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):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)
@app.route("/_revert", methods=['POST'])
def revert():
@app.route("/_revert", methods=['POST'])
def revert():
if request.method == 'POST':
name = request.form.get('name')
commit = request.form.get('commit')
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')
return redirect("/" + cname)
@app.route("/_register", methods=['GET', 'POST'])
def register():
@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'))
@ -268,8 +246,9 @@ def create_app(subdomain=None):
else:
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 User.auth(request.form['email'], request.form['password']):
return redirect(redirect_url(referrer=url_for('root')))
@ -279,20 +258,23 @@ def create_app(subdomain=None):
else:
return render_template('account/login.html')
@app.route("/_history/<name>")
def history(name):
history = w.get_history(name)
@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 = w.get_page(name)
@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():
w.rename_page(cname, edit_cname)
w.write_page(edit_cname, request.form['content'],
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)
@ -304,22 +286,25 @@ def create_app(subdomain=None):
else:
return redirect('/_create/'+cname)
@app.route("/_delete/<name>", methods=['POST'])
@login_required
def delete(name):
@app.route("/_delete/<name>", methods=['POST'])
@login_required
def delete(name):
pass
@app.route("/_create/", methods=['GET', 'POST'])
@app.route("/_create/<name>", methods=['GET', 'POST'])
def create(name=None):
@app.route("/_create/", methods=['GET', 'POST'])
@app.route("/_create/<name>", methods=['GET', 'POST'])
def create(name=None):
cname = ""
if name:
cname = to_canonical(name)
if w.get_page(cname):
if Wiki.get_page(cname):
# Page exists, edit instead
return redirect("/edit/" + cname)
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'],
create=True,
username=g.current_user.get('username'))
@ -327,16 +312,15 @@ def create_app(subdomain=None):
else:
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)
if cname != name:
return redirect('/' + cname)
data = w.get_page(cname)
data = Wiki.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
from realms import config
from sqlalchemy import create_engine
# 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
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 re
import lxml.html
from lxml.html import clean
import ghdiff
import gittle.utils
from gittle import Gittle
from dulwich.repo import NotGitRepository
from werkzeug.utils import escape, unescape
from util import to_canonical
from models import Site
from realms.models import Site
class MyGittle(Gittle):
@ -94,7 +92,7 @@ class Wiki():
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)
content = lxml.html.tostring(tree, encoding='utf-8', method='html')

View file

@ -1,40 +1,12 @@
import rethinkdb as rdb
import bcrypt
from sqlalchemy import Column, Integer, String, Time
from sqlalchemy.ext.declarative import declarative_base
from flask import session, flash
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
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)
Base = declarative_base()
class CurrentUser():
@ -66,8 +38,22 @@ class CurrentUser():
return None
class User(BaseModel):
table = 'users'
class Site(Base):
__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):
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-Login==0.2.7
beautifulsoup4==4.3.2
boto==2.13.3
boto==2.17.0
closure==20121212
gevent==0.13.8
ghdiff==0.1
gittle==0.2.2
itsdangerous==0.23
lxml==3.2.3
lxml==3.2.4
markdown2==2.1.0
pyzmq==13.0.0
pyzmq==14.0.0
recaptcha==1.0rc1
recaptcha-client==1.0.6
redis==2.8.0
rethinkdb==1.10.0-0
simplejson==3.3.0
simplejson==3.3.1
sockjs-tornado==1.0.0
supervisor==3.0
SQLAlchemy==0.8.3
tornado==3.1.1
tldextract==1.2.2
psycopg2==2.5.1

View file

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