WIP
This commit is contained in:
parent
d0777e2b85
commit
b02d3db684
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -5,5 +5,6 @@
|
|||
*.pyc
|
||||
config.py
|
||||
config.sls
|
||||
config.json
|
||||
realms/static/vendor
|
||||
realms/static/assets/*
|
||||
realms/static/assets/*
|
||||
|
|
14
Vagrantfile
vendored
14
Vagrantfile
vendored
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
14
manage.py
14
manage.py
|
@ -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()
|
||||
|
|
|
@ -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('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
|
||||
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():
|
||||
return redirect(url_for(config.ROOT_ENDPOINT))
|
||||
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()
|
||||
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
@ -134,4 +121,4 @@ def to_canonical(s):
|
|||
|
||||
|
||||
def gravatar_url(email):
|
||||
return "//www.gravatar.com/avatar/" + hashlib.md5(email).hexdigest()
|
||||
return "//www.gravatar.com/avatar/" + hashlib.md5(email).hexdigest()
|
||||
|
|
|
@ -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')
|
||||
f.write(content)
|
||||
f.close()
|
||||
with open(self.path + "/" + filename, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
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:
|
||||
|
|
124
realms/models.py
124
realms/models.py
|
@ -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)
|
18
realms/modules/auth/forms.py
Normal file
18
realms/modules/auth/forms.py
Normal 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])
|
||||
|
||||
|
110
realms/modules/auth/models.py
Normal file
110
realms/modules/auth/models.py
Normal 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()
|
||||
|
|
@ -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')))
|
||||
else:
|
||||
flash("Email or Password invalid")
|
||||
return redirect(url_for(".login"))
|
||||
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:
|
||||
return render_template('auth/login.html')
|
||||
return render_template("auth/register.html")
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
@ -32,14 +32,15 @@ def revert():
|
|||
username=g.current_user.get('username'))
|
||||
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:
|
||||
|
|
7
realms/static/css/bootstrap/darkly.css
Normal file
7
realms/static/css/bootstrap/darkly.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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 {
|
||||
|
@ -120,4 +153,20 @@ body {
|
|||
.navbar-nav>li.user-avatar a {
|
||||
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;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
$(function () {
|
||||
|
||||
var url_prefix = "/wiki";
|
||||
var url_prefix = "";
|
||||
|
||||
var $theme = $('#theme-list')
|
||||
, $preview = $('#preview')
|
||||
|
|
|
@ -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 %}
|
|
@ -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" />
|
||||
|
|
|
@ -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 %}
|
|
@ -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">
|
||||
<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 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') %}
|
||||
<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> Login</a></li>
|
||||
<li><a href="{{ url_for('auth.register') }}"><i class="icon-pencil"></i> 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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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">
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
{% endif %}
|
||||
|
||||
TogetherJSConfig_on_ready = function () {
|
||||
MDR.sanitize = true;
|
||||
$("#preview").html('');
|
||||
MDR.sanitize = true;
|
||||
$("#preview").html('');
|
||||
$("#start-togetherjs").addClass('btn-danger').html('End Collaboration').prop('disabled', false);
|
||||
};
|
||||
TogetherJSConfig_on_close = function () {
|
||||
|
@ -47,13 +47,10 @@
|
|||
{% 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>
|
||||
<input id="page-name" type="text" class="form-control input-sm" name="name" placeholder="Name" value="{{- name -}}" />
|
||||
</div>
|
||||
<div class="col-xs-3">
|
||||
<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>
|
||||
<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 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>
|
||||
|
||||
{% endblock %}
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 %}
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||
CREATE USER deploy WITH PASSWORD 'dbpassword';
|
||||
GRANT ALL PRIVILEGES ON DATABASE "realms" to deploy;
|
|
@ -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:
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
/etc/supervisor/conf.d/realms.conf:
|
||||
file.managed:
|
||||
- source: salt://supervisor/supervisord.conf
|
|
@ -1,3 +0,0 @@
|
|||
[program:realms]
|
||||
user=deploy
|
||||
command=/home/deploy/realms/virtualenvs/realms/bin/python /home/deploy/realms/manage.py server
|
|
@ -3,7 +3,4 @@ base:
|
|||
- common
|
||||
- users
|
||||
- redis
|
||||
- nginx
|
||||
- postgres
|
||||
- realms
|
||||
- supervisor
|
|
@ -29,7 +29,7 @@ sudo:
|
|||
- user: deploy
|
||||
- group: deploy
|
||||
- require:
|
||||
- file.directory: /home/deploy
|
||||
- file: /home/deploy
|
||||
|
||||
/home/deploy/.bashrc:
|
||||
file.copy:
|
||||
|
|
Loading…
Reference in a new issue