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
config.py
config.sls
config.json
realms/static/vendor
realms/static/assets/*

14
Vagrantfile vendored
View file

@ -1,7 +1,14 @@
VAGRANTFILE_API_VERSION = "2"
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 ".", "/home/deploy/realms"
config.vm.provision :salt do |salt|
@ -11,7 +18,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
end
Vagrant::Config.run do |config|
config.vm.forward_port 80, 8000
config.vm.forward_port 5432, 5432
config.vm.forward_port 10000, 10000
config.vm.forward_port 80, 8080
config.vm.forward_port 4567, 4567
end

View file

@ -11,6 +11,7 @@
"components-font-awesome": "~3.2.1",
"showdown": "~0.3.1",
"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
def server(port=10000):
print "Server started (%s)" % config.ENV
wsgi.WSGIServer(('', int(port)), app).serve_forever()
def server():
print "Server started. Env: %s Port: %s" % (config.ENV, config.PORT)
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__':
manager.run()

View file

@ -22,55 +22,28 @@ import httplib
import traceback
from flask import Flask, request, render_template, url_for, redirect, session, flash, g
from flask.ctx import _AppCtxGlobals
from flask.ext.script import Manager, Server
from flask.ext.login import LoginManager, login_required
from flask.ext.assets import Environment, Bundle
from flask.ext.script import Manager
from flask.ext.login import LoginManager
from werkzeug.routing import BaseConverter
from werkzeug.utils import cached_property
from werkzeug.exceptions import HTTPException
from realms import config
from realms.lib.services import db
from realms.lib.ratelimit import get_view_rate_limit, ratelimiter
from realms.lib.session import RedisSessionInterface
from realms.lib.wiki import Wiki
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):
@cached_property
def current_site(self):
subdomain = format_subdomain(self.current_subdomain)
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]
def current_user(self):
return session.get('user') if session.get('user') else {'username': 'Anon'}
@cached_property
def current_wiki(self):
return g.current_site.wiki
@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'}
return Wiki(config.WIKI_PATH)
class Application(Flask):
@ -95,20 +68,11 @@ class Application(Flask):
return super(Application, self).__call__(environ, start_response)
def discover(self):
"""
Pattern taken from guildwork.com
"""
IMPORT_NAME = 'realms.modules'
FROMLIST = (
'assets',
'models',
'search',
'perms',
'broadcasts',
'commands',
'notifications',
'requests',
'tasks',
'models',
'views',
)
@ -158,39 +122,18 @@ def redirect_url(referrer=None):
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.config.from_object('realms.config')
app.session_interface = RedisSessionInterface()
app.url_map.converters['regex'] = RegexConverter
app.url_map.strict_slashes = False
app.debug = True
# Flask-SQLAlchemy
db.init_app(app)
app.debug = config.DEBUG
manager = Manager(app)
manager.add_command("runserver", Server(host="0.0.0.0", port=10000))
login_manager = LoginManager()
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):
@ -224,30 +167,28 @@ for status_code in httplib.responses:
if status_code >= 400:
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.app = app
assets.debug = config.DEBUG
app.jinja_env.globals['bundles'] = assets
register(
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/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'
)
'vendor/parsleyjs/dist/parsley.js',
'js/main.js')
@app.before_request
def check_subdomain():
if not g.current_site:
return redirect('http://%s' % config.DOMAIN)
def init_g():
g.assets = ['main']
@app.after_request
@ -271,30 +212,11 @@ def page_not_found(e):
return render_template('errors/404.html'), 404
@app.route("/")
def root():
if config.RELATIVE_PATH:
@app.route("/")
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()

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-'):
ENV = 'PROD'
else:
ENV = 'DEV'
DEBUG = True
ASSETS_DEBUG = True
SQLALCHEMY_DATABASE_URI = 'postgresql://deploy:dbpassword@localhost:5432/realms'
PORT = 80
BASE_URL = 'http://realms.dev'
REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379
REDIS_DB = '0'
SECRET_KEY = 'K3dRq1q9eN72GJDkgvyshFVwlqHHCyPI'
SECRET = 'K3dRq1q9eN72GJDkgvyshFVwlqHHCyPI'
REPO_DIR = '/home/deploy/repos'
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_PATH = '/home/deploy/wiki'
WIKI_HOME = 'home'
MODULES = [
'wiki',
'auth'
]
ALLOW_ANON = True
if ENV is 'PROD':
PORT = 80
DOMAIN = 'realms.io'
else:
DEBUG = True
ASSETS_DEBUG = True
DOMAIN = 'realms.dev'
PORT = 8000
ROOT_ENDPOINT = 'wiki.page'
with open(os.path.join(os.path.dirname(__file__) + "/../../", 'config.json')) as f:
__settings = json.load(f)
globals().update(__settings)
# String trailing slash
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
# This can be done better, make it better
assets = Environment()
filters = 'uglifyjs'
output = 'assets/%(version)s.js'
def register(*files):
assets.debug = True
filters = 'uglifyjs'
output = 'assets/%(version)s.js'
assets.add(Bundle(*files, filters=filters, output=output))
def register(name, *files):
assets.register(name, Bundle(*files, filters=filters, output=output))

View file

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

View file

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

View file

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

View file

@ -67,7 +67,7 @@ class Wiki():
if not page:
# Page not found
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']
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)
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.close()
if create:
self.repo.add(filename)
@ -118,7 +117,7 @@ class Wiki():
username = self.default_committer_name
if not email:
email = "%s@realms.io" % username
email = self.default_committer_email
return self.repo.commit(name=username,
email=email,
@ -135,7 +134,9 @@ class Wiki():
def get_page(self, name, sha='HEAD'):
# 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:
return self.repo.get_commit_files(sha, paths=[name]).get(name)
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 realms import redirect_url
from realms.models import User
from flask import g, render_template, request, redirect, Blueprint, flash, url_for
from realms.modules.auth.models import User
from realms.modules.auth.forms import LoginForm, RegistrationForm
from realms import config
blueprint = Blueprint('auth', __name__)
blueprint = Blueprint('auth', __name__, url_prefix=config.RELATIVE_PATH)
@blueprint.route("/logout/")
def logout():
@blueprint.route("/logout")
def logout_page():
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'])
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'])
@blueprint.route("/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']):
return redirect(redirect_url(referrer=url_for('root')))
return redirect(request.args.get("next") or url_for(config.ROOT_ENDPOINT))
return render_template("auth/login.html")
@blueprint.route("/register")
def register():
if request.method == "POST":
return redirect(request.args.get("next") or url_for(config.ROOT_ENDPOINT))
else:
flash("Email or Password invalid")
return redirect(url_for(".login"))
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
register(
'editor',
'js/ace/ace.js',
'js/ace/mode-markdown.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.ext.login import login_required
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):
cname = to_canonical(name)
@ -16,13 +16,13 @@ def commit(name, sha):
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):
diff = g.current_wiki.compare(name, fsha, 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():
if request.method == 'POST':
name = request.form.get('name')
@ -33,13 +33,14 @@ def revert():
flash('Page reverted', 'success')
return redirect(url_for('wiki.page', name=cname))
@blueprint.route("/wiki/_history/<name>")
@blueprint.route("/_history/<name>")
def 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'))
@blueprint.route("/wiki/_edit/<name>", methods=['GET', 'POST'])
@blueprint.route("/_edit/<name>", methods=['GET', 'POST'])
def edit(name):
data = g.current_wiki.get_page(name)
cname = to_canonical(name)
@ -56,19 +57,19 @@ def edit(name):
if data:
name = remove_ext(data['name'])
content = data['data']
g.assets.append('editor')
return render_template('wiki/edit.html', name=name, content=content)
else:
return redirect(url_for('wiki.create', name=cname))
@blueprint.route("/wiki/_delete/<name>", methods=['POST'])
@login_required
@blueprint.route("/_delete/<name>", methods=['POST'])
def delete(name):
pass
@blueprint.route("/wiki/_create/", defaults={'name': None}, methods=['GET', 'POST'])
@blueprint.route("/wiki/_create/<name>", methods=['GET', 'POST'])
@blueprint.route("/_create/", defaults={'name': None}, methods=['GET', 'POST'])
@blueprint.route("/_create/<name>", methods=['GET', 'POST'])
def create(name):
if request.method == 'POST':
g.current_wiki.write_page(request.form['name'],
@ -82,11 +83,12 @@ def create(name):
# Page exists, edit instead
return redirect(url_for('wiki.edit', name=cname))
g.assets.append('editor')
return render_template('wiki/edit.html', name=cname, content="")
@blueprint.route("/wiki", defaults={'name': 'home'})
@blueprint.route("/wiki/<name>")
@blueprint.route("/", defaults={'name': 'home'})
@blueprint.route("/<name>")
def page(name):
cname = to_canonical(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 {
padding-top: 43px;
background-color:#eee;
}
.navbar {
min-height: inherit;
}
.navbar .container a {
padding-top: 10px;
padding-bottom: 10px;
height: 50px !important;
min-height: 49px !important;
font-size: 0.85em;
background: #242628;
margin-bottom: 10px;
}
.navbar-nav>li>a:hover {
padding-top: 10px;
padding-bottom: 10px;
.navbar .nav li a, .navbar .nav li button, .navbar-brand {
display: block;
height: 49px;
padding: 15px 15px;
border-bottom: none;
color: #7d878a;
text-transform: uppercase;
}
#main-body {
background-color: #fff;
padding: 20px;
margin: 0 -20px;
-webkit-border-radius: 0 0 6px 6px;
border-radius: 0 0 6px 6px;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15);
box-shadow: 0 1px 2px rgba(0,0,0,.15);
padding-top: 10px;
.navbar .nav li a:hover, .navbar-brand:hover {
color: #FFF !important;
}
.navbar .nav li {
font-size: 1em;
position: relative;
border-right: #35393b 1px solid;
}
.navbar-brand {
color: white;
}
.navbar {
border-radius: 0;
}
.checkbox-cell {
@ -34,9 +43,9 @@ body {
#app-wrap {
top: 60px;
left: 10px;
left: -5px;
bottom: 0;
right: 10px;
right: -5px;
position: fixed;
}
@ -45,15 +54,22 @@ body {
}
#preview {
/*
position: absolute;
margin-left: 5px;
padding: 20px;
left: 50%;
bottom: 10px;
right: 10px;
top: 50px;
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;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
@ -72,6 +88,14 @@ body {
}
#editor {
position: absolute;
height: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
/*
position: absolute;
margin-right: 5px;
top: 50px;
@ -91,6 +115,15 @@ body {
box-flex: 1;
-webkit-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 {
@ -121,3 +154,19 @@ body {
padding-top: 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 () {
var url_prefix = "/wiki";
var url_prefix = "";
var $theme = $('#theme-list')
, $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' %}
{% block body %}
<h2>Login</h2>
<form role="form" method="post">
<form role="form" method="post" action="{{ url_for('auth.login') }}" data-parsley-validate>
<div class="form-group">
<label for="email">Email Address</label>
<input type="text" class="form-control" id="email" name="email" />
<label for="email">Email</label>
<input id="email" type="email" class="form-control" name="email" placeholder="Email" required />
</div>
<div class="form-group">
<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>
<input type="submit" class="btn btn-primary" value="Login" />

View file

@ -1,32 +1,29 @@
{% import 'macros.html' as macros %}
{% extends 'layout.html' %}
{% block body %}
<h2>Register</h2>
<form role="form" method="post">
<form role="form" method="post" action="{{ url_for('auth.register') }}" data-parsley-validate>
<div class="form-group">
<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 class="form-group">
<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 class="form-group">
<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 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>
<input type="submit" class="btn btn-primary" value="Submit" />
<input type="submit" class="btn btn-primary" value="Register" />
</form>
<a href="/login" class="pull-right">Already registered? Login here.</a>
{% endblock %}

View file

@ -8,7 +8,7 @@
<title>Realms</title>
<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='vendor/highlightjs/styles/github.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
@ -24,7 +24,7 @@
<body>
<!-- Fixed navbar -->
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
@ -36,21 +36,13 @@
</div>
<div class="navbar-collapse collapse navbar-inverse-collapse">
<ul class="nav navbar-nav">
</ul>
<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>
<li><a href="{{ url_for('wiki.create') }}">New</a></li>
{% if name %}
<li><a href="{{ url_for('wiki.edit', name=name) }}">Edit Page</a></li>
<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>
</li>
<ul class="nav navbar-nav navbar-right">
{% if session.get('user') %}
<li class="dropdown user-avatar">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
@ -65,8 +57,8 @@
</ul>
</li>
{% else %}
<li><a href="{{ url_for('auth.login') }}"><i class="icon-user"></i> Login</a></li>
<li><a href="{{ url_for('auth.register') }}"><i class="icon-pencil"></i> Register</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> &nbsp;Register</a></li>
{% endif %}
</ul>
</div><!--/.nav-collapse -->
@ -96,8 +88,7 @@
{% block body %}{% endblock %}
</div>
</div>
{% for bundle in bundles %}
{% for bundle in g.assets %}
{% assets bundle %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% 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">
<div class="form-group">
<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 class="form-group">

View file

@ -47,14 +47,11 @@
{% endblock %}
<div id="app-wrap" class="container-fluid">
<div id="app-wrap">
<div id="app-controls" class="row">
<div class="col-xs-3">
<div class="input-group">
<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 class="col-xs-3">
<input id="page-message" type="text" class="form-control input-sm" name="page-message" placeholder="Comment" value="" />
</div>
@ -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>
<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/clouds" class="">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/cobalt" class="">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/chrome" >Chrome</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" >Clouds Midnight</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" >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/dreamweaver" class="">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/idle_fingers" class="">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/merbivore" class="">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/mono_industrial" class="">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/dreamweaver" >Dreamweaver</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" >idleFingers</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" >Merbivore</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" >Mono Industrial</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/solarized_dark" class="">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/textmate" class="">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/solarized_dark" >Solarized Dark</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" >TextMate</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_blue" class="">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_eighties" class="">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/vibrant_ink" class="">Vibrant Ink</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" >Tomorrow Night Bright</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" >Twilight</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/vibrant_ink" >Vibrant Ink</a></li>
</ul>
<a id="save-native" class="btn btn-primary btn-sm"><i class="icon-save"></i> Save</a>
</div>
</div>
</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>
{% endblock %}

View file

@ -44,7 +44,6 @@ $(function(){
$(".compare-revisions").click(function(){
var $cs = $('.revision-tbl').find(':checkbox:checked');
console.log($cs.length);
if ($cs.length != 2) return;
var revs = [];
$.each($cs, function(i, v){
@ -52,7 +51,7 @@ $(function(){
});
revs.reverse();
revs = revs.join("..");
location.href = "{{ url_for('wiki.page') }}/_compare/{{ name }}/" + revs;
location.href = "{{ config.BASE_URL }}/_compare/{{ name }}/" + revs;
});
});
</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-Assets
Flask-Bcrypt
Flask-Login
Flask-Assets
Flask-Script
Flask-SQLAlchemy
Flask-WTF
beautifulsoup4
boto
closure==20121212
gevent
ghdiff
@ -13,10 +12,5 @@ gittle
itsdangerous
lxml
markdown2
pyzmq
recaptcha
recaptcha-client
redis
simplejson
SQLAlchemy
psycopg2

View file

@ -11,30 +11,14 @@ python-redis-lea-repo:
pkgrepo.managed:
- 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:
pkg.installed:
- pkgs:
- python
- vim
- build-essential
- screen
- htop
- git
- ntp
- libpcre3-dev
- libevent-dev
- iptraf
- python-software-properties
- python-pip
- python-virtualenv
@ -43,11 +27,10 @@ common-pkgs:
- curl
- libxml2-dev
- libxslt1-dev
- zlib1g-dev
- libffi-dev
- nodejs
- supervisor
- require:
- pkgrepo.managed: nodejs-lea-repo
- pkgrepo.managed: redis-lea-repo
- pkgrepo.managed: python-redis-lea-repo
- pkgrepo.managed: nginx-stable-repo
- pkgrepo.managed: postgres-repo
- pkgrepo: nodejs-lea-repo
- pkgrepo: redis-lea-repo
- pkgrepo: python-redis-lea-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:
- user: root
- require:
- pkg.installed: common-pkgs
- pkg: common-pkgs
uglify-js:
npm.installed:
- user: root
- require:
- pkg.installed: common-pkgs
- pkg: common-pkgs
create_virtualenv:
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
- users
- redis
- nginx
- postgres
- realms
- supervisor

View file

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