This commit is contained in:
Matthew Scragg 2014-08-20 10:28:25 -05:00
parent d0777e2b85
commit b02d3db684
41 changed files with 426 additions and 647 deletions

1
.gitignore vendored
View file

@ -5,5 +5,6 @@
*.pyc *.pyc
config.py config.py
config.sls config.sls
config.json
realms/static/vendor realms/static/vendor
realms/static/assets/* realms/static/assets/*

14
Vagrantfile vendored
View file

@ -1,7 +1,14 @@
VAGRANTFILE_API_VERSION = "2" VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "precise64" config.vm.box = "ubuntu/trusty64"
config.vm.provider :virtualbox do |vb|
vb.name = "realms-wiki"
vb.memory = 2048
vb.cpus = 2
end
config.vm.synced_folder "srv/", "/srv/" config.vm.synced_folder "srv/", "/srv/"
config.vm.synced_folder ".", "/home/deploy/realms" config.vm.synced_folder ".", "/home/deploy/realms"
config.vm.provision :salt do |salt| config.vm.provision :salt do |salt|
@ -11,7 +18,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
end end
Vagrant::Config.run do |config| Vagrant::Config.run do |config|
config.vm.forward_port 80, 8000 config.vm.forward_port 80, 8080
config.vm.forward_port 5432, 5432 config.vm.forward_port 4567, 4567
config.vm.forward_port 10000, 10000
end end

View file

@ -11,6 +11,7 @@
"components-font-awesome": "~3.2.1", "components-font-awesome": "~3.2.1",
"showdown": "~0.3.1", "showdown": "~0.3.1",
"keymaster": "madrobby/keymaster", "keymaster": "madrobby/keymaster",
"ace": "~1.1.0" "ace": "~1.1.0",
"parsleyjs": "~2.0.3"
} }
} }

View file

@ -3,18 +3,10 @@ from realms import config, app, manager
@manager.command @manager.command
def server(port=10000): def server():
print "Server started (%s)" % config.ENV print "Server started. Env: %s Port: %s" % (config.ENV, config.PORT)
wsgi.WSGIServer(('', int(port)), app).serve_forever() wsgi.WSGIServer(('', int(config.PORT)), app).serve_forever()
@manager.command
def init_db():
from realms import db
import realms.models
print "Init DB"
db.drop_all()
db.create_all()
if __name__ == '__main__': if __name__ == '__main__':
manager.run() manager.run()

View file

@ -22,55 +22,28 @@ import httplib
import traceback import traceback
from flask import Flask, request, render_template, url_for, redirect, session, flash, g from flask import Flask, request, render_template, url_for, redirect, session, flash, g
from flask.ctx import _AppCtxGlobals from flask.ctx import _AppCtxGlobals
from flask.ext.script import Manager, Server from flask.ext.script import Manager
from flask.ext.login import LoginManager, login_required from flask.ext.login import LoginManager
from flask.ext.assets import Environment, Bundle
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from werkzeug.utils import cached_property from werkzeug.utils import cached_property
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from realms import config from realms import config
from realms.lib.services import db
from realms.lib.ratelimit import get_view_rate_limit, ratelimiter from realms.lib.ratelimit import get_view_rate_limit, ratelimiter
from realms.lib.session import RedisSessionInterface from realms.lib.session import RedisSessionInterface
from realms.lib.wiki import Wiki from realms.lib.wiki import Wiki
from realms.lib.util import to_canonical, remove_ext, mkdir_safe, gravatar_url, to_dict from realms.lib.util import to_canonical, remove_ext, mkdir_safe, gravatar_url, to_dict
from realms.models import User, CurrentUser, Site
sites = {}
class AppCtxGlobals(_AppCtxGlobals): class AppCtxGlobals(_AppCtxGlobals):
@cached_property @cached_property
def current_site(self): def current_user(self):
subdomain = format_subdomain(self.current_subdomain) return session.get('user') if session.get('user') else {'username': 'Anon'}
if not subdomain:
subdomain = "www"
if subdomain is "www" and self.current_subdomain:
# Invalid sub domain
return False
if not sites.get(subdomain):
sites[subdomain] = to_dict(Site.get_by_name(subdomain))
sites[subdomain].wiki = Wiki("%s/%s" % (config.REPO_DIR, subdomain))
return sites[subdomain]
@cached_property @cached_property
def current_wiki(self): def current_wiki(self):
return g.current_site.wiki return Wiki(config.WIKI_PATH)
@cached_property
def current_subdomain(self):
host = request.host.split(':')[0]
return host[:-len(config.DOMAIN)].rstrip('.')
@cached_property
def current_user(self):
return session.get('user') if session.get('user') else {'username': 'Anon'}
class Application(Flask): class Application(Flask):
@ -95,20 +68,11 @@ class Application(Flask):
return super(Application, self).__call__(environ, start_response) return super(Application, self).__call__(environ, start_response)
def discover(self): def discover(self):
"""
Pattern taken from guildwork.com
"""
IMPORT_NAME = 'realms.modules' IMPORT_NAME = 'realms.modules'
FROMLIST = ( FROMLIST = (
'assets', 'assets',
'models',
'search',
'perms',
'broadcasts',
'commands', 'commands',
'notifications', 'models',
'requests',
'tasks',
'views', 'views',
) )
@ -158,39 +122,18 @@ def redirect_url(referrer=None):
return request.args.get('next') or referrer or url_for('index') return request.args.get('next') or referrer or url_for('index')
def format_subdomain(s):
if not config.REPO_ENABLE_SUBDOMAIN:
return ""
s = s.lower()
s = to_canonical(s)
if s in config.REPO_FORBIDDEN_NAMES:
# Not allowed
s = ""
return s
app = Application(__name__) app = Application(__name__)
app.config.from_object('realms.config') app.config.from_object('realms.config')
app.session_interface = RedisSessionInterface() app.session_interface = RedisSessionInterface()
app.url_map.converters['regex'] = RegexConverter app.url_map.converters['regex'] = RegexConverter
app.url_map.strict_slashes = False app.url_map.strict_slashes = False
app.debug = True app.debug = config.DEBUG
# Flask-SQLAlchemy
db.init_app(app)
manager = Manager(app) manager = Manager(app)
manager.add_command("runserver", Server(host="0.0.0.0", port=10000))
login_manager = LoginManager() login_manager = LoginManager()
login_manager.init_app(app) login_manager.init_app(app)
login_manager.login_view = 'auth.login'
@login_manager.user_loader
def load_user(user_id):
return CurrentUser(user_id)
def error_handler(e): def error_handler(e):
@ -224,30 +167,28 @@ for status_code in httplib.responses:
if status_code >= 400: if status_code >= 400:
app.register_error_handler(status_code, error_handler) app.register_error_handler(status_code, error_handler)
from realms.lib.assets import assets, register from realms.lib.assets import register, assets
assets.init_app(app) assets.init_app(app)
assets.app = app assets.app = app
assets.debug = config.DEBUG
app.jinja_env.globals['bundles'] = assets register('main',
'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',
'js/html-sanitizer-minified.js', # don't minify
'vendor/highlightjs/highlight.pack.js',
'vendor/parsleyjs/dist/parsley.js',
'js/main.js')
register(
'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/marked/lib/marked.js',
'vendor/showdown/src/extensions/table.js',
'js/wmd.js',
'js/html-sanitizer-minified.js', # don't minify
'vendor/highlightjs/highlight.pack.js',
'js/main.js'
)
@app.before_request @app.before_request
def check_subdomain(): def init_g():
if not g.current_site: g.assets = ['main']
return redirect('http://%s' % config.DOMAIN)
@app.after_request @app.after_request
@ -271,30 +212,11 @@ def page_not_found(e):
return render_template('errors/404.html'), 404 return render_template('errors/404.html'), 404
@app.route("/") if config.RELATIVE_PATH:
def root(): @app.route("/")
return redirect(url_for(config.ROOT_ENDPOINT)) def root():
return redirect(url_for(config.ROOT_ENDPOINT))
@app.route("/new/", methods=['GET', 'POST'])
@login_required
def new():
if request.method == 'POST':
site_name = to_canonical(request.form['name'])
if Site.get_by_name(site_name):
flash("Site already exists")
return redirect(redirect_url())
else:
Site.create(name=site_name, founder=g.current_user.id)
return redirect('http://%s.%s' % (site_name, config.HOSTNAME))
else:
return render_template('wiki/new.html')
@app.route("/_account/")
@login_required
def account():
return render_template('account/index.html')
app.discover() app.discover()

View file

@ -1,41 +1,41 @@
import socket import os
import json
from urlparse import urlparse
HOSTNAME = socket.gethostname() ENV = 'DEV'
if HOSTNAME.startswith('lsn-'): DEBUG = True
ENV = 'PROD' ASSETS_DEBUG = True
else:
ENV = 'DEV'
SQLALCHEMY_DATABASE_URI = 'postgresql://deploy:dbpassword@localhost:5432/realms' PORT = 80
BASE_URL = 'http://realms.dev'
REDIS_HOST = '127.0.0.1' REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379 REDIS_PORT = 6379
REDIS_DB = '0'
SECRET_KEY = 'K3dRq1q9eN72GJDkgvyshFVwlqHHCyPI' SECRET = 'K3dRq1q9eN72GJDkgvyshFVwlqHHCyPI'
REPO_DIR = '/home/deploy/repos' WIKI_PATH = '/home/deploy/wiki'
REPO_MAIN_NAME = '_'
REPO_FORBIDDEN_NAMES = ['api', 'www']
REPO_ENABLE_SUBDOMAIN = True
RECAPTCHA_PUBLIC_KEY = '6LfoxeESAAAAAGNaeWnISh0GTgDk0fBnr6Bo2Tfk'
RECAPTCHA_PRIVATE_KEY = '6LfoxeESAAAAABFzdCs0hNIIyeb42mofV-Ndd2_2'
RECAPTCHA_OPTIONS = {'theme': 'clean'}
ROOT_ENDPOINT = 'wiki.page'
WIKI_HOME = 'home' WIKI_HOME = 'home'
MODULES = [ ALLOW_ANON = True
'wiki',
'auth'
]
if ENV is 'PROD': ROOT_ENDPOINT = 'wiki.page'
PORT = 80
DOMAIN = 'realms.io' with open(os.path.join(os.path.dirname(__file__) + "/../../", 'config.json')) as f:
else: __settings = json.load(f)
DEBUG = True globals().update(__settings)
ASSETS_DEBUG = True
DOMAIN = 'realms.dev' # String trailing slash
PORT = 8000 if BASE_URL.endswith('/'):
BASE_URL = BASE_URL[-1]
_url = urlparse(BASE_URL)
RELATIVE_PATH = _url.path
if ENV != "DEV":
DEBUG = False
ASSETS_DEBUG = False
MODULES = ['wiki', 'auth']

View file

@ -1,10 +1,11 @@
from flask.ext.assets import Bundle, Environment from flask.ext.assets import Bundle, Environment
# This can be done better, make it better
assets = Environment() assets = Environment()
filters = 'uglifyjs'
output = 'assets/%(version)s.js'
def register(*files): def register(name, *files):
assets.debug = True assets.register(name, Bundle(*files, filters=filters, output=output))
filters = 'uglifyjs'
output = 'assets/%(version)s.js'
assets.add(Bundle(*files, filters=filters, output=output))

View file

@ -1,7 +1,7 @@
import time import time
from functools import update_wrapper from functools import update_wrapper
from flask import request, g from flask import request, g
from services import cache from services import db
class RateLimit(object): class RateLimit(object):
@ -13,7 +13,7 @@ class RateLimit(object):
self.limit = limit self.limit = limit
self.per = per self.per = per
self.send_x_headers = send_x_headers self.send_x_headers = send_x_headers
p = cache.pipeline() p = db.pipeline()
p.incr(self.key) p.incr(self.key)
p.expireat(self.key, self.reset + self.expiration_window) p.expireat(self.key, self.reset + self.expiration_window)
self.current = min(p.execute()[0], limit) self.current = min(p.execute()[0], limit)

View file

@ -1,8 +1,4 @@
import redis import redis
from flask.ext.sqlalchemy import SQLAlchemy
from realms import config from realms import config
db = SQLAlchemy() db = redis.StrictRedis(host=config.REDIS_HOST, port=config.REDIS_PORT, db=config.REDIS_DB)
# Default Cache connection
cache = redis.StrictRedis(host=config.REDIS_HOST, port=config.REDIS_PORT)

View file

@ -3,11 +3,7 @@ import os
import hashlib import hashlib
import json import json
from flask import request from realms.lib.services import db
from recaptcha.client import captcha
from realms import config
from realms.lib.services import cache
class AttrDict(dict): class AttrDict(dict):
@ -41,7 +37,7 @@ def to_dict(data):
def cache_it(fn): def cache_it(fn):
def wrap(*args, **kw): def wrap(*args, **kw):
key = "%s:%s" % (args[0].table, args[1]) key = "%s:%s" % (args[0].table, args[1])
data = cache.get(key) data = db.get(key)
# Assume strings are JSON encoded # Assume strings are JSON encoded
try: try:
data = json.loads(data) data = json.loads(data)
@ -63,20 +59,11 @@ def cache_it(fn):
data = json.dumps(data, separators=(',', ':')) data = json.dumps(data, separators=(',', ':'))
except TypeError: except TypeError:
pass pass
cache.set(key, data) db.set(key, data)
return ret return ret
return wrap return wrap
def validate_captcha():
response = captcha.submit(
request.form['recaptcha_challenge_field'],
request.form['recaptcha_response_field'],
config.RECAPTCHA_PRIVATE_KEY,
request.remote_addr)
return response.is_valid
def mkdir_safe(path): def mkdir_safe(path):
if path and not(os.path.exists(path)): if path and not(os.path.exists(path)):
os.makedirs(path) os.makedirs(path)

View file

@ -67,7 +67,7 @@ class Wiki():
if not page: if not page:
# Page not found # Page not found
return None return None
commit_info = gittle.utils.git.commit_info(self.repo[commit_sha]) commit_info = gittle.utils.git.commit_info(self.repo[commit_sha.encode('latin-1')])
message = commit_info['message'] 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)
@ -104,9 +104,8 @@ class Wiki():
content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL) content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL)
filename = self.cname_to_filename(to_canonical(name)) filename = self.cname_to_filename(to_canonical(name))
f = open(self.path + "/" + filename, 'w') with open(self.path + "/" + filename, 'w') as f:
f.write(content) f.write(content)
f.close()
if create: if create:
self.repo.add(filename) self.repo.add(filename)
@ -118,7 +117,7 @@ class Wiki():
username = self.default_committer_name username = self.default_committer_name
if not email: if not email:
email = "%s@realms.io" % username email = self.default_committer_email
return self.repo.commit(name=username, return self.repo.commit(name=username,
email=email, email=email,
@ -135,7 +134,9 @@ class Wiki():
def get_page(self, name, sha='HEAD'): def get_page(self, name, sha='HEAD'):
# commit = gittle.utils.git.commit_info(self.repo[sha]) # commit = gittle.utils.git.commit_info(self.repo[sha])
name = self.cname_to_filename(name) name = self.cname_to_filename(name).encode('latin-1')
sha = sha.encode('latin-1')
try: try:
return self.repo.get_commit_files(sha, paths=[name]).get(name) return self.repo.get_commit_files(sha, paths=[name]).get(name)
except KeyError: except KeyError:

View file

@ -1,124 +0,0 @@
import bcrypt
from sqlalchemy import Column, Integer, String, Time
from flask import session, flash
from flask.ext.login import login_user, logout_user
from realms.lib.services import db
from realms.lib.util import gravatar_url, to_dict
class ModelMixin(object):
def __getitem__(self, k):
return self.__getattribute__(k)
@classmethod
def create(cls, **kwargs):
obj = cls(**kwargs)
db.session.add(obj)
db.session.commit()
return obj
class CurrentUser():
id = None
def __init__(self, id):
self.id = id
if id:
session['user'] = to_dict(User.query.filter_by(id=id).first())
def get_id(self):
return self.id
def is_active(self):
return True if self.id else False
def is_anonymous(self):
return False if self.id else True
def is_authenticated(self):
return True if self.id else False
@staticmethod
def get(key):
try:
return session['user'][key]
except KeyError:
return None
class Site(ModelMixin, db.Model):
__tablename__ = 'sites'
id = Column(Integer, primary_key=True)
name = Column(String(100))
pages = Column(Integer)
views = Column(Integer)
founder = Column(Integer)
created_at = Column(Time)
updated_at = Column(Time)
@classmethod
def get_by_name(cls, name):
return Site.query.filter_by(name=name).first()
class User(db.Model, ModelMixin):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(100))
email = Column(String(255))
avatar = Column(String(255))
password = Column(String(255))
created_at = Column(Time)
updated_at = Column(Time)
@classmethod
def get_by_email(cls, email):
return User.query.filter_by(email=email).first()
@classmethod
def get_by_username(cls, username):
return User.query.filter_by(username=username).first()
def login(self, login, password):
pass
@classmethod
def auth(cls, username, password):
u = User()
data = u.get_by_email(username)
if not data:
return False
if bcrypt.checkpw(password, data['password']):
cls.login(data['id'])
return True
else:
return False
@classmethod
def register(cls, username, email, password):
user = User()
email = email.lower()
if user.get_by_email(email):
flash('Email is already taken')
return False
if user.get_by_username(username):
flash('Username is already taken')
return False
# Create user and login
u = User.create(email=email,
username=username,
password=bcrypt.hashpw(password, bcrypt.gensalt(10)),
avatar=gravatar_url(email))
User.login(u.id)
@classmethod
def login(cls, id):
login_user(CurrentUser(id), remember=True)
@classmethod
def logout(cls):
logout_user()
session.pop('user', None)

View file

@ -0,0 +1,18 @@
from wtforms import Form, StringField, PasswordField, validators
class RegistrationForm(Form):
username = StringField('Username', [validators.Length(min=4, max=25)])
email = StringField('Email Address', [validators.Length(min=6, max=35)])
password = PasswordField('Password', [
validators.DataRequired(),
validators.EqualTo('confirm', message='Passwords must match')
])
confirm = PasswordField('Repeat Password')
class LoginForm(Form):
email = StringField('Email', [validators.DataRequired])
password = PasswordField('Password', [validators.DataRequired])

View file

@ -0,0 +1,110 @@
from flask.ext.login import UserMixin, logout_user, login_user
from realms import config, login_manager
from realms.lib.services import db
from itsdangerous import URLSafeSerializer, BadSignature
from hashlib import sha256
import json
import bcrypt
FIELD_MAP = dict(
u='username',
e='email',
p='password',
nv='not_verified',
a='admin',
b='banned')
@login_manager.user_loader
def load_user(user_id):
return User.get(user_id)
@login_manager.token_loader
def load_token(token):
# Load unsafe because payload is needed for sig
sig_okay, payload = URLSafeSerializer(None).load_unsafe(token)
if not payload:
return False
# User key *could* be stored in payload to avoid user lookup in db
user = User.get(payload.get('id'))
if not user:
return False
try:
if User.signer(sha256(user.password).hexdigest()).loads(token):
return user
else:
return False
except BadSignature:
return False
class User(UserMixin):
username = None
email = None
password = None
def __init__(self, email, data=None):
self.id = email
for k, v in data.items():
setattr(self, FIELD_MAP.get(k, k), v)
def get_auth_token(self):
key = sha256(self.password).hexdigest()
return User.signer(key).dumps(dict(id=self.username))
@staticmethod
def create(username, email, password):
User.set(email, dict(u=username, e=email, p=User.hash(password), nv=1))
@staticmethod
def signer(salt):
"""
Signed with app secret salted with sha256 of password hash of user (client secret)
"""
return URLSafeSerializer(config.SECRET + salt)
@staticmethod
def set(email, data):
db.set('u:%s' % email, json.dumps(data, separators=(',', ':')))
@staticmethod
def get(email):
data = db.get('u:%s', email)
try:
data = json.loads(data)
except ValueError:
return None
if data:
return User(email, data)
else:
return None
@staticmethod
def auth(email, password):
user = User.get(email)
if not user:
return False
if bcrypt.checkpw(password, user.password):
login_user(user, remember=True)
return user
else:
return False
@staticmethod
def hash(password):
return bcrypt.hashpw(password, bcrypt.gensalt(log_rounds=12))
@classmethod
def logout(cls):
logout_user()

View file

@ -1,35 +1,36 @@
from flask import render_template, redirect, request, url_for, flash, Blueprint from flask import g, render_template, request, redirect, Blueprint, flash, url_for
from realms import redirect_url from realms.modules.auth.models import User
from realms.models import User from realms.modules.auth.forms import LoginForm, RegistrationForm
from realms import config
blueprint = Blueprint('auth', __name__) blueprint = Blueprint('auth', __name__, url_prefix=config.RELATIVE_PATH)
@blueprint.route("/logout/") @blueprint.route("/logout")
def logout(): def logout_page():
User.logout() User.logout()
return redirect(url_for('root')) flash("You are now logged out")
return redirect(url_for(config.ROOT_ENDPOINT))
@blueprint.route("/register/", methods=['GET', 'POST']) @blueprint.route("/login")
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('auth/register.html')
@blueprint.route("/login/", methods=['GET', 'POST'])
def login(): def login():
if request.method == 'POST': if request.method == "POST":
form = RegistrationForm()
# TODO
if not form.validate():
flash('Form invalid')
return redirect(url_for('auth.login'))
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(request.args.get("next") or url_for(config.ROOT_ENDPOINT))
else:
flash("Email or Password invalid") return render_template("auth/login.html")
return redirect(url_for(".login"))
@blueprint.route("/register")
def register():
if request.method == "POST":
return redirect(request.args.get("next") or url_for(config.ROOT_ENDPOINT))
else: else:
return render_template('auth/login.html') return render_template("auth/register.html")

View file

@ -1,8 +1,8 @@
from realms.lib.assets import register from realms.lib.assets import register
register( register(
'editor',
'js/ace/ace.js', 'js/ace/ace.js',
'js/ace/mode-markdown.js', 'js/ace/mode-markdown.js',
'vendor/keymaster/keymaster.js', 'vendor/keymaster/keymaster.js',
'js/dillinger.js' 'js/dillinger.js')
)

View file

@ -1,11 +1,11 @@
from flask import g, render_template, request, redirect, Blueprint, flash, url_for from flask import g, render_template, request, redirect, Blueprint, flash, url_for
from flask.ext.login import login_required
from realms.lib.util import to_canonical, remove_ext from realms.lib.util import to_canonical, remove_ext
from realms import config
blueprint = Blueprint('wiki', __name__) blueprint = Blueprint('wiki', __name__, url_prefix=config.RELATIVE_PATH)
@blueprint.route("/wiki/_commit/<sha>/<name>") @blueprint.route("/_commit/<sha>/<name>")
def commit(name, sha): def commit(name, sha):
cname = to_canonical(name) cname = to_canonical(name)
@ -16,13 +16,13 @@ def commit(name, sha):
return redirect(url_for('wiki.create', name=cname)) return redirect(url_for('wiki.create', name=cname))
@blueprint.route("/wiki/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>") @blueprint.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
def compare(name, fsha, dots, lsha): def compare(name, fsha, dots, lsha):
diff = g.current_wiki.compare(name, fsha, 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("/wiki/_revert", methods=['POST']) @blueprint.route("/_revert", methods=['POST'])
def revert(): def revert():
if request.method == 'POST': if request.method == 'POST':
name = request.form.get('name') name = request.form.get('name')
@ -33,13 +33,14 @@ def revert():
flash('Page reverted', 'success') flash('Page reverted', 'success')
return redirect(url_for('wiki.page', name=cname)) return redirect(url_for('wiki.page', name=cname))
@blueprint.route("/wiki/_history/<name>")
@blueprint.route("/_history/<name>")
def history(name): def history(name):
history = g.current_wiki.get_history(name) history = g.current_wiki.get_history(name)
return render_template('wiki/history.html', name=name, history=history, wiki_home=url_for('wiki.page')) return render_template('wiki/history.html', name=name, history=history, wiki_home=url_for('wiki.page'))
@blueprint.route("/wiki/_edit/<name>", methods=['GET', 'POST']) @blueprint.route("/_edit/<name>", methods=['GET', 'POST'])
def edit(name): def edit(name):
data = g.current_wiki.get_page(name) data = g.current_wiki.get_page(name)
cname = to_canonical(name) cname = to_canonical(name)
@ -56,19 +57,19 @@ def edit(name):
if data: if data:
name = remove_ext(data['name']) name = remove_ext(data['name'])
content = data['data'] content = data['data']
g.assets.append('editor')
return render_template('wiki/edit.html', name=name, content=content) return render_template('wiki/edit.html', name=name, content=content)
else: else:
return redirect(url_for('wiki.create', name=cname)) return redirect(url_for('wiki.create', name=cname))
@blueprint.route("/wiki/_delete/<name>", methods=['POST']) @blueprint.route("/_delete/<name>", methods=['POST'])
@login_required
def delete(name): def delete(name):
pass pass
@blueprint.route("/wiki/_create/", defaults={'name': None}, methods=['GET', 'POST']) @blueprint.route("/_create/", defaults={'name': None}, methods=['GET', 'POST'])
@blueprint.route("/wiki/_create/<name>", methods=['GET', 'POST']) @blueprint.route("/_create/<name>", methods=['GET', 'POST'])
def create(name): def create(name):
if request.method == 'POST': if request.method == 'POST':
g.current_wiki.write_page(request.form['name'], g.current_wiki.write_page(request.form['name'],
@ -82,11 +83,12 @@ def create(name):
# Page exists, edit instead # Page exists, edit instead
return redirect(url_for('wiki.edit', name=cname)) return redirect(url_for('wiki.edit', name=cname))
g.assets.append('editor')
return render_template('wiki/edit.html', name=cname, content="") return render_template('wiki/edit.html', name=cname, content="")
@blueprint.route("/wiki", defaults={'name': 'home'}) @blueprint.route("/", defaults={'name': 'home'})
@blueprint.route("/wiki/<name>") @blueprint.route("/<name>")
def page(name): def page(name):
cname = to_canonical(name) cname = to_canonical(name)
if cname != name: if cname != name:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,30 +1,39 @@
body { body {
padding-top: 43px;
background-color:#eee;
} }
.navbar { .navbar {
min-height: inherit; height: 50px !important;
} min-height: 49px !important;
.navbar .container a { font-size: 0.85em;
padding-top: 10px; background: #242628;
padding-bottom: 10px; margin-bottom: 10px;
} }
.navbar-nav>li>a:hover { .navbar .nav li a, .navbar .nav li button, .navbar-brand {
padding-top: 10px; display: block;
padding-bottom: 10px; height: 49px;
padding: 15px 15px;
border-bottom: none;
color: #7d878a;
text-transform: uppercase;
} }
#main-body { .navbar .nav li a:hover, .navbar-brand:hover {
background-color: #fff; color: #FFF !important;
padding: 20px; }
margin: 0 -20px;
-webkit-border-radius: 0 0 6px 6px; .navbar .nav li {
border-radius: 0 0 6px 6px; font-size: 1em;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15); position: relative;
box-shadow: 0 1px 2px rgba(0,0,0,.15); border-right: #35393b 1px solid;
padding-top: 10px; }
.navbar-brand {
color: white;
}
.navbar {
border-radius: 0;
} }
.checkbox-cell { .checkbox-cell {
@ -34,9 +43,9 @@ body {
#app-wrap { #app-wrap {
top: 60px; top: 60px;
left: 10px; left: -5px;
bottom: 0; bottom: 0;
right: 10px; right: -5px;
position: fixed; position: fixed;
} }
@ -45,15 +54,22 @@ body {
} }
#preview { #preview {
/*
position: absolute; position: absolute;
margin-left: 5px; margin-left: 5px;
padding: 20px;
left: 50%; left: 50%;
bottom: 10px; bottom: 10px;
right: 10px; right: 10px;
top: 50px; top: 50px;
overflow: auto; overflow: auto;
background: rgba(255,255,255,0.9); */
position: absolute;
height: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 20px;
border: 1px solid #EEE; border: 1px solid #EEE;
-webkit-border-radius: 4px; -webkit-border-radius: 4px;
-moz-border-radius: 4px; -moz-border-radius: 4px;
@ -72,6 +88,14 @@ body {
} }
#editor { #editor {
position: absolute;
height: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
/*
position: absolute; position: absolute;
margin-right: 5px; margin-right: 5px;
top: 50px; top: 50px;
@ -91,6 +115,15 @@ body {
box-flex: 1; box-flex: 1;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15); -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15);
box-shadow: 0 1px 2px rgba(0,0,0,.15); box-shadow: 0 1px 2px rgba(0,0,0,.15);
*/
font-family: Inconsolata, monospace;
font-size: 1.2em;
line-height: 1.3em;
}
.ace_gutter-cell {
font-size: 1em;
line-height: 1em;
} }
#page-action-bar { #page-action-bar {
@ -121,3 +154,19 @@ body {
padding-top: 9px; padding-top: 9px;
padding-bottom: 9px; padding-bottom: 9px;
} }
.floating-header {
position: absolute;
right: 12px;
bottom: -39px;
z-index: 400;
/* height: 20px; */
padding: 1px;
text-transform: uppercase;
color: #aaa9a2;
background-color: #000;
border: 1px solid #000;
/* background-image: -webkit-linear-gradient(top, #fff 0%,#fff 25%,rgba(255,255,255,0.9) 100%); */
/* background-image: linear-gradient(to bottom,#fff 0%,#fff 25%,rgba(255,255,255,0.9) 100%); */
font-size: 10px;
}

View file

@ -1,6 +1,6 @@
$(function () { $(function () {
var url_prefix = "/wiki"; var url_prefix = "";
var $theme = $('#theme-list') var $theme = $('#theme-list')
, $preview = $('#preview') , $preview = $('#preview')

View file

@ -1,14 +0,0 @@
{% extends 'layout.html' %}
{% block body %}
<h2>Account</h2>
<form method="POST" role="form">
<div class="form-group">
<label for="email" class="control-label">Email</label>
<input id="email" type="text" class="form-control" value="{{ session['user']['email'] }}" />
</div>
<input type="submit" class="btn btn-primary" value="Save">
</form>
{% endblock %}

View file

@ -1,17 +1,15 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block body %} {% block body %}
<h2>Login</h2> <form role="form" method="post" action="{{ url_for('auth.login') }}" data-parsley-validate>
<form role="form" method="post">
<div class="form-group"> <div class="form-group">
<label for="email">Email Address</label> <label for="email">Email</label>
<input type="text" class="form-control" id="email" name="email" /> <input id="email" type="email" class="form-control" name="email" placeholder="Email" required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" name="password" id="password" class="form-control" /> <input type="password" name="password" id="password" class="form-control" placeholder="Password" min="6" required />
</div> </div>
<input type="submit" class="btn btn-primary" value="Login" /> <input type="submit" class="btn btn-primary" value="Login" />

View file

@ -1,32 +1,29 @@
{% import 'macros.html' as macros %}
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block body %} {% block body %}
<h2>Register</h2> <form role="form" method="post" action="{{ url_for('auth.register') }}" data-parsley-validate>
<form role="form" method="post">
<div class="form-group"> <div class="form-group">
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" /> <input id="username" type="text" class="form-control" name="username" placeholder="Username" required data-parsley-type="alphanum" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="email">Email</label> <label for="email">Email</label>
<input type="text" class="form-control" id="email" name="email" /> <input id="email" type="email" class="form-control" name="email" placeholder="Email" required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" name="password" id="password" class="form-control" /> <input type="password" name="password" id="password" class="form-control" placeholder="Password" required min="6"/>
</div> </div>
<div class="form-group"> <div class="form-group">
{{ macros.recaptcha(config) }} <label for="password_again">Confirm Password</label>
<input type="password" name="password_again" id="password_again" class="form-control" placeholder="Password" required min="6"/>
</div> </div>
<input type="submit" class="btn btn-primary" value="Submit" />
<input type="submit" class="btn btn-primary" value="Register" />
</form> </form>
<a href="/login" class="pull-right">Already registered? Login here.</a>
{% endblock %} {% endblock %}

View file

@ -8,7 +8,7 @@
<title>Realms</title> <title>Realms</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.ico') }}"> <link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
<link href="{{ url_for('static', filename='css/bootstrap/spacelab.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='css/bootstrap/flatly.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='vendor/highlightjs/styles/github.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='vendor/highlightjs/styles/github.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
@ -24,7 +24,7 @@
<body> <body>
<!-- Fixed navbar --> <!-- Fixed navbar -->
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="navbar navbar-inverse">
<div class="container"> <div class="container">
<div class="navbar-header"> <div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
@ -36,21 +36,13 @@
</div> </div>
<div class="navbar-collapse collapse navbar-inverse-collapse"> <div class="navbar-collapse collapse navbar-inverse-collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li><a href="{{ url_for('wiki.create') }}">New</a></li>
{% if name %}
<li><a href="{{ url_for('wiki.edit', name=name) }}">Edit</a></li>
<li><a href="{{ url_for('wiki.history', name=name) }}">History</a></li>
{% endif %}
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" role="menu" data-toggle="dropdown">Write
<i class="icon-caret-down"></i></a>
<ul class="dropdown-menu">
<li class="dropdown-header">Page Options</li>
<li><a href="{{ url_for('wiki.create') }}">Create Page</a></li>
{% if name %}
<li><a href="{{ url_for('wiki.edit', name=name) }}">Edit Page</a></li>
<li><a href="{{ url_for('wiki.history', name=name) }}">History</a></li>
{% endif %}
</ul>
</li>
{% if session.get('user') %} {% if session.get('user') %}
<li class="dropdown user-avatar"> <li class="dropdown user-avatar">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">
@ -65,8 +57,8 @@
</ul> </ul>
</li> </li>
{% else %} {% else %}
<li><a href="{{ url_for('auth.login') }}"><i class="icon-user"></i> Login</a></li> <li><a href="{{ url_for('auth.login') }}"><i class="icon-user"></i> &nbsp;Login</a></li>
<li><a href="{{ url_for('auth.register') }}"><i class="icon-pencil"></i> Register</a></li> <li><a href="{{ url_for('auth.register') }}"><i class="icon-pencil"></i> &nbsp;Register</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div><!--/.nav-collapse --> </div><!--/.nav-collapse -->
@ -96,8 +88,7 @@
{% block body %}{% endblock %} {% block body %}{% endblock %}
</div> </div>
</div> </div>
{% for bundle in g.assets %}
{% for bundle in bundles %}
{% assets bundle %} {% assets bundle %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script> <script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% endassets %}

View file

@ -1,9 +0,0 @@
{% macro recaptcha(config) -%}
<script>
var RecaptchaOptions = {
theme : "{{ config.RECAPTCHA_OPTIONS['theme'] }}"
};
</script>
<script src="http://www.google.com/recaptcha/api/challenge?k={{ config.RECAPTCHA_PUBLIC_KEY }}">
</script>
{%- endmacro %}

View file

@ -4,7 +4,7 @@
<form role="form" method="post"> <form role="form" method="post">
<div class="form-group"> <div class="form-group">
<label for="name"></label> <label for="name"></label>
<input id="name" type="text" class="form-control" id="page" name="name" placeholder="Name" value="{{- name -}}" /> <input id="name" type="text" class="form-control" name="name" placeholder="Page Name" value="{{- name -}}" />
</div> </div>
<div class="form-group"> <div class="form-group">

View file

@ -19,8 +19,8 @@
{% endif %} {% endif %}
TogetherJSConfig_on_ready = function () { TogetherJSConfig_on_ready = function () {
MDR.sanitize = true; MDR.sanitize = true;
$("#preview").html(''); $("#preview").html('');
$("#start-togetherjs").addClass('btn-danger').html('End Collaboration').prop('disabled', false); $("#start-togetherjs").addClass('btn-danger').html('End Collaboration').prop('disabled', false);
}; };
TogetherJSConfig_on_close = function () { TogetherJSConfig_on_close = function () {
@ -47,13 +47,10 @@
{% endblock %} {% endblock %}
<div id="app-wrap" class="container-fluid"> <div id="app-wrap">
<div id="app-controls" class="row"> <div id="app-controls" class="row">
<div class="col-xs-3"> <div class="col-xs-3">
<div class="input-group"> <input id="page-name" type="text" class="form-control input-sm" name="name" placeholder="Name" value="{{- name -}}" />
<span class="input-group-addon btn-info input-sm">realms.io/wiki/</span>
<input id="page-name" type="text" class="form-control input-sm" name="name" placeholder="Name" value="{{- name -}}" />
</div>
</div> </div>
<div class="col-xs-3"> <div class="col-xs-3">
<input id="page-message" type="text" class="form-control input-sm" name="page-message" placeholder="Comment" value="" /> <input id="page-message" type="text" class="form-control input-sm" name="page-message" placeholder="Comment" value="" />
@ -69,38 +66,47 @@
<a href="#" id="drop6" role="button" class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown">Theme <b class="caret"></b></a> <a href="#" id="drop6" role="button" class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown">Theme <b class="caret"></b></a>
<ul id="theme-list" class="dropdown-menu" role="menu" aria-labelledby="drop6"> <ul id="theme-list" class="dropdown-menu" role="menu" aria-labelledby="drop6">
<li><a tabindex="-1" href="#" data-value="ace/theme/chrome" class="">Chrome</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/chrome" >Chrome</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/clouds" class="">Clouds</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/clouds" >Clouds</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/clouds_midnight" class="">Clouds Midnight</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/clouds_midnight" >Clouds Midnight</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/cobalt" class="">Cobalt</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/cobalt" >Cobalt</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/crimson_editor" class="">Crimson Editor</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/crimson_editor" >Crimson Editor</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/dawn" class="selected">Dawn</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/dawn" class="selected">Dawn</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/dreamweaver" class="">Dreamweaver</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/dreamweaver" >Dreamweaver</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/eclipse" class="">Eclipse</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/eclipse" >Eclipse</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/idle_fingers" class="">idleFingers</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/idle_fingers" >idleFingers</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/kr_theme" class="">krTheme</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/kr_theme" >krTheme</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/merbivore" class="">Merbivore</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/merbivore" >Merbivore</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/merbivore_soft" class="">Merbivore Soft</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/merbivore_soft" >Merbivore Soft</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/mono_industrial" class="">Mono Industrial</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/mono_industrial" >Mono Industrial</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/monokai" class="">Monokai</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/monokai" >Monokai</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/pastel_on_dark">Pastel on Dark</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/pastel_on_dark">Pastel on Dark</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/solarized_dark" class="">Solarized Dark</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/solarized_dark" >Solarized Dark</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/solarized_light" class="">Solarized Light</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/solarized_light" >Solarized Light</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/textmate" class="">TextMate</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/textmate" >TextMate</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow" class="">Tomorrow</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow" >Tomorrow</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night">Tomorrow Night</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night">Tomorrow Night</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night_blue" class="">Tomorrow Night Blue</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night_blue" >Tomorrow Night Blue</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night_bright" class="">Tomorrow Night Bright</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night_bright" >Tomorrow Night Bright</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night_eighties" class="">Tomorrow Night 80s</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/tomorrow_night_eighties" >Tomorrow Night 80s</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/twilight" class="">Twilight</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/twilight" >Twilight</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/vibrant_ink" class="">Vibrant Ink</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/vibrant_ink" >Vibrant Ink</a></li>
</ul> </ul>
<a id="save-native" class="btn btn-primary btn-sm"><i class="icon-save"></i> Save</a> <a id="save-native" class="btn btn-primary btn-sm"><i class="icon-save"></i> Save</a>
</div> </div>
</div> </div>
</div> </div>
<div id="editor">{{ content }}</div>
<div id="preview"></div> <div class="row" style="position: relative; height: 100%; margin-left: 15px; margin-right: 15px;">
<div class="col-xs-6" style="position: absolute; bottom: 20px; top: 10px; left:0;border-right: 2px solid transparent;">
<div id="editor">{{ content }}</div>
</div>
<div class="col-xs-6" style="position: absolute; bottom: 20px; top: 10px; right:0; border-left: 2px solid transparent;">
<div id="preview"></div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -44,7 +44,6 @@ $(function(){
$(".compare-revisions").click(function(){ $(".compare-revisions").click(function(){
var $cs = $('.revision-tbl').find(':checkbox:checked'); var $cs = $('.revision-tbl').find(':checkbox:checked');
console.log($cs.length);
if ($cs.length != 2) return; if ($cs.length != 2) return;
var revs = []; var revs = [];
$.each($cs, function(i, v){ $.each($cs, function(i, v){
@ -52,7 +51,7 @@ $(function(){
}); });
revs.reverse(); revs.reverse();
revs = revs.join(".."); revs = revs.join("..");
location.href = "{{ url_for('wiki.page') }}/_compare/{{ name }}/" + revs; location.href = "{{ config.BASE_URL }}/_compare/{{ name }}/" + revs;
}); });
}); });
</script> </script>

View file

@ -1,19 +0,0 @@
{% extends 'layout.html' %}
{% block body %}
<h2>Create New Site</h2>
<div class="row">
<div class='col-md-6'>
<form method="POST" role="form">
<div class="form-group">
<label for="wiki" class="control-label">Site Name</label>
<div class="input-group">
<input id="wiki" name="name" type="text" class="form-control" value="{{ request.args.get('site', '') }}" />
<span class="input-group-addon">.realms.io</span>
</div>
</div>
<input type="submit" class="btn btn-primary" value="Save">
</form>
</div>
</div>
{% endblock %}

View file

@ -1,11 +1,10 @@
bcrypt
Flask Flask
Flask-Assets
Flask-Bcrypt
Flask-Login Flask-Login
Flask-Assets
Flask-Script Flask-Script
Flask-SQLAlchemy Flask-WTF
beautifulsoup4 beautifulsoup4
boto
closure==20121212 closure==20121212
gevent gevent
ghdiff ghdiff
@ -13,10 +12,5 @@ gittle
itsdangerous itsdangerous
lxml lxml
markdown2 markdown2
pyzmq
recaptcha
recaptcha-client
redis redis
simplejson simplejson
SQLAlchemy
psycopg2

View file

@ -11,30 +11,14 @@ python-redis-lea-repo:
pkgrepo.managed: pkgrepo.managed:
- ppa: chris-lea/python-redis - ppa: chris-lea/python-redis
nginx-stable-repo:
pkgrepo.managed:
- ppa: nginx/stable
- required_in: nginx
postgres-repo:
pkgrepo.managed:
- name: deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main
- key_url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
- required_in: postgresql
common-pkgs: common-pkgs:
pkg.installed: pkg.installed:
- pkgs: - pkgs:
- python - python
- vim
- build-essential - build-essential
- screen
- htop
- git - git
- ntp
- libpcre3-dev - libpcre3-dev
- libevent-dev - libevent-dev
- iptraf
- python-software-properties - python-software-properties
- python-pip - python-pip
- python-virtualenv - python-virtualenv
@ -43,11 +27,10 @@ common-pkgs:
- curl - curl
- libxml2-dev - libxml2-dev
- libxslt1-dev - libxslt1-dev
- zlib1g-dev
- libffi-dev
- nodejs - nodejs
- supervisor
- require: - require:
- pkgrepo.managed: nodejs-lea-repo - pkgrepo: nodejs-lea-repo
- pkgrepo.managed: redis-lea-repo - pkgrepo: redis-lea-repo
- pkgrepo.managed: python-redis-lea-repo - pkgrepo: python-redis-lea-repo
- pkgrepo.managed: nginx-stable-repo
- pkgrepo.managed: postgres-repo

View file

@ -1,15 +0,0 @@
nginx:
pkg:
- installed
service.running:
- enable: True
- reload: True
- require:
- pkg: nginx
- watch:
- file: /etc/nginx/conf.d/realms.conf
/etc/nginx/conf.d/realms.conf:
file.managed:
- template: jinja
- source: salt://nginx/nginx.conf

View file

@ -1,71 +0,0 @@
{% set root = '/home/deploy/realms/realms' %}
{% set ssl_certificate = False %}
gzip_proxied any;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
upstream web {
fair;
server 127.0.0.1:10000;
}
server {
listen 80;
# Allow file uploads
client_max_body_size 50M;
location ^~ /static/ {
root {{ root }};
expires max;
}
location = /favicon.ico {
rewrite (.*) /static/favicon.ico;
}
location = /robots.txt {
rewrite (.*) /static/robots.txt;
}
location / {
proxy_pass_header Server;
proxy_redirect off;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header Host $http_host;
proxy_pass http://web;
error_page 502 = /maintenance.html;
}
location /maintenance.html {
root {{ root }}/templates/;
add_header Cache-Control private;
expires epoch;
}
}
{% if ssl_certificate %}
server {
listen 443;
ssl on;
ssl_certificate {{ ssl_certificate }};
ssl_certificate_key {{ ssl_certificate_key }};
location ^~ /static/ {
root {{ root }};
expires max;
}
location / {
proxy_pass_header Server;
proxy_redirect off;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header Host $http_host;
proxy_pass http://web;
error_page 502 = /maintenance.html;
}
}
{% endif %}

View file

@ -1,18 +0,0 @@
postgresql:
pkg.installed:
- name: postgresql-9.3
libpq-dev:
pkg.installed
createdb:
cmd.run:
- name: createdb -T template1 realms
- user: postgres
- require:
- pkg.installed: postgresql-9.3
initdb:
cmd.run:
- name: psql realms < /srv/salt/postgres/init.sql
- user: postgres

View file

@ -1,2 +0,0 @@
CREATE USER deploy WITH PASSWORD 'dbpassword';
GRANT ALL PRIVILEGES ON DATABASE "realms" to deploy;

View file

@ -16,13 +16,13 @@ bower:
npm.installed: npm.installed:
- user: root - user: root
- require: - require:
- pkg.installed: common-pkgs - pkg: common-pkgs
uglify-js: uglify-js:
npm.installed: npm.installed:
- user: root - user: root
- require: - require:
- pkg.installed: common-pkgs - pkg: common-pkgs
create_virtualenv: create_virtualenv:
virtualenv.managed: virtualenv.managed:

View file

@ -1,3 +0,0 @@
/etc/supervisor/conf.d/realms.conf:
file.managed:
- source: salt://supervisor/supervisord.conf

View file

@ -1,3 +0,0 @@
[program:realms]
user=deploy
command=/home/deploy/realms/virtualenvs/realms/bin/python /home/deploy/realms/manage.py server

View file

@ -3,7 +3,4 @@ base:
- common - common
- users - users
- redis - redis
- nginx
- postgres
- realms - realms
- supervisor

View file

@ -29,7 +29,7 @@ sudo:
- user: deploy - user: deploy
- group: deploy - group: deploy
- require: - require:
- file.directory: /home/deploy - file: /home/deploy
/home/deploy/.bashrc: /home/deploy/.bashrc:
file.copy: file.copy: