Browse Source

WIP

master
Matthew Scragg 9 years ago
parent
commit
86f0549e44
24 changed files with 710 additions and 398 deletions
  1. +1
    -0
      Vagrantfile
  2. +7
    -1
      manage.py
  3. +72
    -71
      realms/__init__.py
  4. +23
    -6
      realms/config/__init__.py
  5. +0
    -11
      realms/lib/assets.py
  6. +287
    -0
      realms/lib/model.py
  7. +0
    -46
      realms/lib/ratelimit.py
  8. +0
    -4
      realms/lib/services.py
  9. +0
    -64
      realms/lib/session.py
  10. +0
    -32
      realms/lib/util.py
  11. +8
    -4
      realms/modules/auth/forms.py
  12. +49
    -47
      realms/modules/auth/models.py
  13. +40
    -9
      realms/modules/auth/views.py
  14. +6
    -7
      realms/modules/wiki/assets.py
  15. +18
    -8
      realms/modules/wiki/models.py
  16. +0
    -5
      realms/modules/wiki/tests.py
  17. +33
    -26
      realms/modules/wiki/views.py
  18. +39
    -8
      realms/static/css/style.css
  19. +6
    -16
      realms/templates/auth/login.html
  20. +10
    -26
      realms/templates/auth/register.html
  21. +5
    -0
      realms/templates/auth/settings.html
  22. +4
    -6
      realms/templates/layout.html
  23. +99
    -0
      realms/templates/macros.html
  24. +3
    -1
      requirements.txt

+ 1
- 0
Vagrantfile View File

@@ -11,6 +11,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

config.vm.synced_folder "srv/", "/srv/"
config.vm.synced_folder ".", "/home/deploy/realms"
config.vm.synced_folder "~/.virtualenvs", "/home/deploy/virtualenvs"
config.vm.provision :salt do |salt|
salt.minion_config = "srv/minion"
salt.run_highstate = true


+ 7
- 1
manage.py View File

@@ -1,9 +1,15 @@
from gevent import wsgi
from realms import config, app, manager
from flask.ext.script import Server

manager.add_command("runserver", Server(host="0.0.0.0", port=config.PORT))


@manager.command
def server():
def run():
"""
Run production ready server
"""
print "Server started. Env: %s Port: %s" % (config.ENV, config.PORT)
wsgi.WSGIServer(('', int(config.PORT)), app).serve_forever()



+ 72
- 71
realms/__init__.py View File

@@ -20,18 +20,18 @@ import sys
import json
import httplib
import traceback
from flask import Flask, request, render_template, url_for, redirect, session, flash, g
from flask import Flask, request, render_template, url_for, redirect, g
from flask.ctx import _AppCtxGlobals
from flask.ext.cache import Cache
from flask.ext.script import Manager
from flask.ext.login import LoginManager
from flask.ext.login import LoginManager, current_user
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.assets import Environment, Bundle
from werkzeug.routing import BaseConverter
from werkzeug.utils import cached_property
from werkzeug.exceptions import HTTPException

from realms import config
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


@@ -39,11 +39,7 @@ class AppCtxGlobals(_AppCtxGlobals):

@cached_property
def current_user(self):
return session.get('user') if session.get('user') else {'username': 'Anon'}

@cached_property
def current_wiki(self):
return Wiki(config.WIKI_PATH)
return current_user


class Application(Flask):
@@ -68,8 +64,8 @@ class Application(Flask):
return super(Application, self).__call__(environ, start_response)

def discover(self):
IMPORT_NAME = 'realms.modules'
FROMLIST = (
import_name = 'realms.modules'
fromlist = (
'assets',
'commands',
'models',
@@ -78,10 +74,10 @@ class Application(Flask):

start_time = time.time()

__import__(IMPORT_NAME, fromlist=FROMLIST)
__import__(import_name, fromlist=fromlist)

for module_name in self.config['MODULES']:
sources = __import__('%s.%s' % (IMPORT_NAME, module_name), fromlist=FROMLIST)
sources = __import__('%s.%s' % (import_name, module_name), fromlist=fromlist)

# Blueprint
if hasattr(sources, 'views'):
@@ -107,6 +103,18 @@ class Application(Flask):
return super(Application, self).make_response(tuple(rv))


class Assets(Environment):
default_filters = {'js': 'uglifyjs', 'css': 'cssmin'}
default_output = {'js': 'assets/%(version)s.js', 'css': 'assets/%(version)s.css'}

def register(self, name, *args, **kwargs):
ext = args[0].split('.')[-1]
filters = kwargs.get('filters', self.default_filters[ext])
output = kwargs.get('output', self.default_output[ext])

super(Assets, self).register(name, Bundle(*args, filters=filters, output=output))


class RegexConverter(BaseConverter):
"""
Enables Regex matching on endpoints
@@ -122,20 +130,6 @@ def redirect_url(referrer=None):
return request.args.get('next') or referrer or url_for('index')



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 = config.DEBUG

manager = Manager(app)

login_manager = LoginManager()
login_manager.init_app(app)


def error_handler(e):
try:
if isinstance(e, HTTPException):
@@ -150,7 +144,7 @@ def error_handler(e):
if request.is_xhr or request.accept_mimetypes.best in ['application/json', 'text/javascript']:
response = {
'message': message,
'traceback': tb,
'traceback': tb
}
else:
response = render_template('errors/error.html',
@@ -163,60 +157,67 @@ def error_handler(e):

return response, status_code

for status_code in httplib.responses:
if status_code >= 400:
app.register_error_handler(status_code, error_handler)

from realms.lib.assets import register, assets
assets.init_app(app)
assets.app = app
assets.debug = config.DEBUG
def create_app():
app = Application(__name__)
app.config.from_object('realms.config')
app.url_map.converters['regex'] = RegexConverter
app.url_map.strict_slashes = False

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')
for status_code in httplib.responses:
if status_code >= 400:
app.register_error_handler(status_code, error_handler)

@app.before_request
def init_g():
g.assets = ['main']

@app.before_request
def init_g():
g.assets = ['main']
@app.template_filter('datetime')
def _jinja2_filter_datetime(ts):
return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts))

@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404

@app.after_request
def inject_x_rate_headers(response):
limit = get_view_rate_limit()
if limit and limit.send_x_headers:
h = response.headers
h.add('X-RateLimit-Remaining', str(limit.remaining))
h.add('X-RateLimit-Limit', str(limit.limit))
h.add('X-RateLimit-Reset', str(limit.reset))
return response
if config.RELATIVE_PATH:
@app.route("/")
def root():
return redirect(url_for(config.ROOT_ENDPOINT))

return app

@app.template_filter('datetime')
def _jinja2_filter_datetime(ts):
return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts))
app = create_app()

# Init plugins here if possible
manager = Manager(app)

login_manager = LoginManager(app)
login_manager.login_view = 'auth.login'

db = SQLAlchemy(app)
cache = Cache(app)

assets = Environment(app)
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')

app.discover()

db.create_all()

@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404


if config.RELATIVE_PATH:
@app.route("/")
def root():
return redirect(url_for(config.ROOT_ENDPOINT))


app.discover()


+ 23
- 6
realms/config/__init__.py View File

@@ -7,19 +7,35 @@ ENV = 'DEV'
DEBUG = True
ASSETS_DEBUG = True

SQLALCHEMY_ECHO = True

PORT = 80
BASE_URL = 'http://realms.dev'

REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379
REDIS_DB = '0'
DB_URI = 'sqlite:////home/deploy/wiki.db'

CACHE_TYPE = 'simple'

# Redis Example
"""
CACHE_TYPE = 'redis'
CACHE_REDIS_HOST = '127.0.0.1'
CACHE_REDIS_PORT = 6379
CACHE_REDIS_DB = '0'
"""

SECRET = 'K3dRq1q9eN72GJDkgvyshFVwlqHHCyPI'
RECAPTCHA_ENABLE = True
RECAPTCHA_USE_SSL = False
RECAPTCHA_PUBLIC_KEY = "6LfYbPkSAAAAAB4a2lG2Y_Yjik7MG9l4TDzyKUao"
RECAPTCHA_PRIVATE_KEY = "6LfYbPkSAAAAAG-KlkwjZ8JLWgwc9T0ytkN7lWRE"
RECAPTCHA_OPTIONS = {}

SECRET_KEY = 'K3dRq1q9eN72GJDkgvyshFVwlqHHCyPI'

WIKI_PATH = '/home/deploy/wiki'
WIKI_HOME = 'home'

ALLOW_ANON = True
LOGIN_DISABLED = ALLOW_ANON

ROOT_ENDPOINT = 'wiki.page'

@@ -27,10 +43,11 @@ with open(os.path.join(os.path.dirname(__file__) + "/../../", 'config.json')) as
__settings = json.load(f)
globals().update(__settings)

# String trailing slash
if BASE_URL.endswith('/'):
BASE_URL = BASE_URL[-1]

SQLALCHEMY_DATABASE_URI = DB_URI

_url = urlparse(BASE_URL)
RELATIVE_PATH = _url.path



+ 0
- 11
realms/lib/assets.py View File

@@ -1,11 +0,0 @@
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(name, *files):
assets.register(name, Bundle(*files, filters=filters, output=output))

+ 287
- 0
realms/lib/model.py View File

@@ -0,0 +1,287 @@
import json
from realms import db
from sqlalchemy import not_
from datetime import datetime


class Model(db.Model):
"""Base SQLAlchemy Model for automatic serialization and
deserialization of columns and nested relationships.

Source: https://gist.github.com/alanhamlett/6604662

Usage::

>>> class User(Model):
>>> id = db.Column(db.Integer(), primary_key=True)
>>> email = db.Column(db.String(), index=True)
>>> name = db.Column(db.String())
>>> password = db.Column(db.String())
>>> posts = db.relationship('Post', backref='user', lazy='dynamic')
>>> ...
>>> default_fields = ['email', 'name']
>>> hidden_fields = ['password']
>>> readonly_fields = ['email', 'password']
>>>
>>> class Post(Model):
>>> id = db.Column(db.Integer(), primary_key=True)
>>> user_id = db.Column(db.String(), db.ForeignKey('user.id'), nullable=False)
>>> title = db.Column(db.String())
>>> ...
>>> default_fields = ['title']
>>> readonly_fields = ['user_id']
>>>
>>> model = User(email='john@localhost')
>>> db.session.add(model)
>>> db.session.commit()
>>>
>>> # update name and create a new post
>>> validated_input = {'name': 'John', 'posts': [{'title':'My First Post'}]}
>>> model.set_columns(**validated_input)
>>> db.session.commit()
>>>
>>> print(model.to_dict(show=['password', 'posts']))
>>> {u'email': u'john@localhost', u'posts': [{u'id': 1, u'title': u'My First Post'}], u'name': u'John', u'id': 1}
"""
__abstract__ = True

# Stores changes made to this model's attributes. Can be retrieved
# with model.changes
_changes = {}

def __init__(self, **kwargs):
kwargs['_force'] = True
self._set_columns(**kwargs)

def _set_columns(self, **kwargs):
force = kwargs.get('_force')

readonly = []
if hasattr(self, 'readonly_fields'):
readonly = self.readonly_fields
if hasattr(self, 'hidden_fields'):
readonly += self.hidden_fields

readonly += [
'id',
'created',
'updated',
'modified',
'created_at',
'updated_at',
'modified_at',
]

changes = {}

columns = self.__table__.columns.keys()
relationships = self.__mapper__.relationships.keys()

for key in columns:
allowed = True if force or key not in readonly else False
exists = True if key in kwargs else False
if allowed and exists:
val = getattr(self, key)
if val != kwargs[key]:
changes[key] = {'old': val, 'new': kwargs[key]}
setattr(self, key, kwargs[key])

for rel in relationships:
allowed = True if force or rel not in readonly else False
exists = True if rel in kwargs else False
if allowed and exists:
is_list = self.__mapper__.relationships[rel].uselist
if is_list:
valid_ids = []
query = getattr(self, rel)
cls = self.__mapper__.relationships[rel].argument()
for item in kwargs[rel]:
if 'id' in item and query.filter_by(id=item['id']).limit(1).count() == 1:
obj = cls.query.filter_by(id=item['id']).first()
col_changes = obj.set_columns(**item)
if col_changes:
col_changes['id'] = str(item['id'])
if rel in changes:
changes[rel].append(col_changes)
else:
changes.update({rel: [col_changes]})
valid_ids.append(str(item['id']))
else:
col = cls()
col_changes = col.set_columns(**item)
query.append(col)
db.session.flush()
if col_changes:
col_changes['id'] = str(col.id)
if rel in changes:
changes[rel].append(col_changes)
else:
changes.update({rel: [col_changes]})
valid_ids.append(str(col.id))

# delete related rows that were not in kwargs[rel]
for item in query.filter(not_(cls.id.in_(valid_ids))).all():
col_changes = {
'id': str(item.id),
'deleted': True,
}
if rel in changes:
changes[rel].append(col_changes)
else:
changes.update({rel: [col_changes]})
db.session.delete(item)

else:
val = getattr(self, rel)
if self.__mapper__.relationships[rel].query_class is not None:
if val is not None:
col_changes = val.set_columns(**kwargs[rel])
if col_changes:
changes.update({rel: col_changes})
else:
if val != kwargs[rel]:
setattr(self, rel, kwargs[rel])
changes[rel] = {'old': val, 'new': kwargs[rel]}

return changes

def set_columns(self, **kwargs):
self._changes = self._set_columns(**kwargs)
if 'modified' in self.__table__.columns:
self.modified = datetime.utcnow()
if 'updated' in self.__table__.columns:
self.updated = datetime.utcnow()
if 'modified_at' in self.__table__.columns:
self.modified_at = datetime.utcnow()
if 'updated_at' in self.__table__.columns:
self.updated_at = datetime.utcnow()
return self._changes

def __repr__(self):
if 'id' in self.__table__.columns.keys():
return '%s(%s)' % (self.__class__.__name__, self.id)
data = {}
for key in self.__table__.columns.keys():
val = getattr(self, key)
if type(val) is datetime:
val = val.strftime('%Y-%m-%dT%H:%M:%SZ')
data[key] = val
return json.dumps(data, use_decimal=True)

@property
def changes(self):
return self._changes

def reset_changes(self):
self._changes = {}

def to_dict(self, show=None, hide=None, path=None, show_all=None):
""" Return a dictionary representation of this model.
"""

if not show:
show = []
if not hide:
hide = []
hidden = []
if hasattr(self, 'hidden_fields'):
hidden = self.hidden_fields
default = []
if hasattr(self, 'default_fields'):
default = self.default_fields

ret_data = {}

if not path:
path = self.__tablename__.lower()
def prepend_path(item):
item = item.lower()
if item.split('.', 1)[0] == path:
return item
if len(item) == 0:
return item
if item[0] != '.':
item = '.%s' % item
item = '%s%s' % (path, item)
return item
show[:] = [prepend_path(x) for x in show]
hide[:] = [prepend_path(x) for x in hide]

columns = self.__table__.columns.keys()
relationships = self.__mapper__.relationships.keys()
properties = dir(self)

for key in columns:
check = '%s.%s' % (path, key)
if check in hide or key in hidden:
continue
if show_all or key is 'id' or check in show or key in default:
ret_data[key] = getattr(self, key)

for key in relationships:
check = '%s.%s' % (path, key)
if check in hide or key in hidden:
continue
if show_all or check in show or key in default:
hide.append(check)
is_list = self.__mapper__.relationships[key].uselist
if is_list:
ret_data[key] = []
for item in getattr(self, key):
ret_data[key].append(item.to_dict(
show=show,
hide=hide,
path=('%s.%s' % (path, key.lower())),
show_all=show_all,
))
else:
if self.__mapper__.relationships[key].query_class is not None:
ret_data[key] = getattr(self, key).to_dict(
show=show,
hide=hide,
path=('%s.%s' % (path, key.lower())),
show_all=show_all,
)
else:
ret_data[key] = getattr(self, key)

for key in list(set(properties) - set(columns) - set(relationships)):
if key.startswith('_'):
continue
check = '%s.%s' % (path, key)
if check in hide or key in hidden:
continue
if show_all or check in show or key in default:
val = getattr(self, key)
try:
ret_data[key] = json.loads(json.dumps(val))
except:
pass

return ret_data

@classmethod
def insert_or_update(cls, cond, data):
obj = cls.query.filter_by(**cond).first()
if obj:
obj.set_columns(**data)
else:
data.update(cond)
obj = cls(**data)
db.session.add(obj)
db.session.commit()

def save(self):
if self not in db.session:
db.session.merge(self)
db.session.commit()

def delete(self):
if self not in db.session:
db.session.merge(self)
db.session.delete(self)
db.session.commit()

@classmethod
def get_by_id(cls, id_):
return cls.query.filter_by(id=id_).first()

+ 0
- 46
realms/lib/ratelimit.py View File

@@ -1,46 +0,0 @@
import time
from functools import update_wrapper
from flask import request, g
from services import db


class RateLimit(object):
expiration_window = 10

def __init__(self, key_prefix, limit, per, send_x_headers):
self.reset = (int(time.time()) // per) * per + per
self.key = key_prefix + str(self.reset)
self.limit = limit
self.per = per
self.send_x_headers = send_x_headers
p = db.pipeline()
p.incr(self.key)
p.expireat(self.key, self.reset + self.expiration_window)
self.current = min(p.execute()[0], limit)

remaining = property(lambda x: x.limit - x.current)
over_limit = property(lambda x: x.current >= x.limit)


def get_view_rate_limit():
return getattr(g, '_view_rate_limit', None)


def on_over_limit(limit):
return 'Slow it down', 400


def ratelimiter(limit, per=300, send_x_headers=True,
over_limit=on_over_limit,
scope_func=lambda: request.remote_addr,
key_func=lambda: request.endpoint):
def decorator(f):
def rate_limited(*args, **kwargs):
key = 'rate-limit/%s/%s/' % (key_func(), scope_func())
rlimit = RateLimit(key, limit, per, send_x_headers)
g._view_rate_limit = rlimit
if over_limit is not None and rlimit.over_limit:
return over_limit(rlimit)
return f(*args, **kwargs)
return update_wrapper(rate_limited, f)
return decorator

+ 0
- 4
realms/lib/services.py View File

@@ -1,4 +0,0 @@
import redis
from realms import config

db = redis.StrictRedis(host=config.REDIS_HOST, port=config.REDIS_PORT, db=config.REDIS_DB)

+ 0
- 64
realms/lib/session.py View File

@@ -1,64 +0,0 @@
import pickle
from datetime import timedelta
from uuid import uuid4
from redis import Redis
from werkzeug.datastructures import CallbackDict
from flask.sessions import SessionInterface, SessionMixin


class RedisSession(CallbackDict, SessionMixin):

def __init__(self, initial=None, sid=None, new=False):
def on_update(self):
self.modified = True
CallbackDict.__init__(self, initial, on_update)
self.sid = sid
self.new = new
self.modified = False


class RedisSessionInterface(SessionInterface):
serializer = pickle
session_class = RedisSession

def __init__(self, redis=None, prefix='session:'):
if redis is None:
redis = Redis()
self.redis = redis
self.prefix = prefix

def generate_sid(self):
return str(uuid4())

def get_redis_expiration_time(self, app, session):
if session.permanent:
return app.permanent_session_lifetime
return timedelta(days=1)

def open_session(self, app, request):
sid = request.cookies.get(app.session_cookie_name)
if not sid:
sid = self.generate_sid()
return self.session_class(sid=sid)
val = self.redis.get(self.prefix + sid)
if val is not None:
data = self.serializer.loads(val)
return self.session_class(data, sid=sid)
return self.session_class(sid=sid, new=True)

def save_session(self, app, session, response):
domain = self.get_cookie_domain(app)
if not session:
self.redis.delete(self.prefix + session.sid)
if session.modified:
response.delete_cookie(app.session_cookie_name,
domain=domain)
return
redis_exp = self.get_redis_expiration_time(app, session)
cookie_exp = self.get_expiration_time(app, session)
val = self.serializer.dumps(dict(session))
self.redis.setex(self.prefix + session.sid, val,
int(redis_exp.total_seconds()))
response.set_cookie(app.session_cookie_name, session.sid,
expires=cookie_exp, httponly=True,
domain=domain)

+ 0
- 32
realms/lib/util.py View File

@@ -3,8 +3,6 @@ import os
import hashlib
import json

from realms.lib.services import db


class AttrDict(dict):
def __init__(self, *args, **kwargs):
@@ -34,36 +32,6 @@ def to_dict(data):
return row2dict(data)


def cache_it(fn):
def wrap(*args, **kw):
key = "%s:%s" % (args[0].table, args[1])
data = db.get(key)
# Assume strings are JSON encoded
try:
data = json.loads(data)
except TypeError:
pass
except ValueError:
pass

if data is not None:
return data
else:
data = fn(*args)
print data
ret = data
if data is None:
data = ''
if not isinstance(data, basestring):
try:
data = json.dumps(data, separators=(',', ':'))
except TypeError:
pass
db.set(key, data)
return ret
return wrap


def mkdir_safe(path):
if path and not(os.path.exists(path)):
os.makedirs(path)


+ 8
- 4
realms/modules/auth/forms.py View File

@@ -1,5 +1,6 @@
from wtforms import Form, StringField, PasswordField, validators

from flask_wtf import Form, RecaptchaField
from wtforms import StringField, PasswordField, validators
from realms import config

class RegistrationForm(Form):
username = StringField('Username', [validators.Length(min=4, max=25)])
@@ -10,9 +11,12 @@ class RegistrationForm(Form):
])
confirm = PasswordField('Repeat Password')

if config.RECAPTCHA_ENABLE:
setattr(RegistrationForm, 'recaptcha', RecaptchaField("You Human?"))


class LoginForm(Form):
email = StringField('Email', [validators.DataRequired])
password = PasswordField('Password', [validators.DataRequired])
email = StringField('Email', [validators.DataRequired()])
password = PasswordField('Password', [validators.DataRequired()])



+ 49
- 47
realms/modules/auth/models.py View File

@@ -1,23 +1,15 @@
from flask.ext.login import UserMixin, logout_user, login_user
from realms import config, login_manager
from realms.lib.services import db
from flask.ext.login import UserMixin, logout_user, login_user, AnonymousUserMixin
from realms import config, login_manager, db
from realms.lib.model import Model
from realms.lib.util import gravatar_url
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)
return User.get_by_id(user_id)


@login_manager.token_loader
@@ -29,7 +21,7 @@ def load_token(token):
return False

# User key *could* be stored in payload to avoid user lookup in db
user = User.get(payload.get('id'))
user = User.get_by_id(payload.get('id'))

if not user:
return False
@@ -43,68 +35,78 @@ def load_token(token):
return False


class User(UserMixin):
class AnonUser(AnonymousUserMixin):
username = 'Anon'
email = ''


username = None
email = None
password = None
class User(Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, unique=True)
email = db.Column(db.String, unique=True)
password = db.Column(db.String)

def __init__(self, email, data=None):
self.id = email
for k, v in data.items():
setattr(self, FIELD_MAP.get(k, k), v)
hidden_fields = ['password']
readonly_fields = ['email', 'password']

def get_auth_token(self):
key = sha256(self.password).hexdigest()
return User.signer(key).dumps(dict(id=self.username))
return User.signer(key).dumps(dict(id=self.id))

@property
def avatar(self):
return gravatar_url(self.email)

@staticmethod
def create(username, email, password):
User.set(email, dict(u=username, e=email, p=User.hash(password), nv=1))
u = User()
u.username = username
u.email = email
u.password = User.hash_password(password)
u.save()

@staticmethod
def signer(salt):
"""
Signed with app secret salted with sha256 of password hash of user (client secret)
"""
return URLSafeSerializer(config.SECRET + salt)
def get_by_username(username):
return User.query.filter_by(username=username).first()

@staticmethod
def set(email, data):
db.set('u:%s' % email, json.dumps(data, separators=(',', ':')))
def get_by_email(email):
return User.query.filter_by(email=email).first()

@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
def signer(salt):
"""
Signed with app secret salted with sha256 of password hash of user (client secret)
"""
return URLSafeSerializer(config.SECRET_KEY + salt)

@staticmethod
def auth(email, password):
user = User.get(email)
user = User.query.filter_by(email=email).first()

if not user:
# User doesn't exist
return False

if bcrypt.checkpw(password, user.password):
if User.check_password(password, user.password):
# Password is good, log in user
login_user(user, remember=True)
return user
else:
# Password check failed
return False

@staticmethod
def hash(password):
return bcrypt.hashpw(password, bcrypt.gensalt(log_rounds=12))
def hash_password(password):
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(12))

@staticmethod
def check_password(password, hashed):
return bcrypt.hashpw(password.encode('utf-8'), hashed.encode('utf-8')) == hashed

@classmethod
def logout(cls):
logout_user()

login_manager.anonymous_user = AnonUser

+ 40
- 9
realms/modules/auth/views.py View File

@@ -13,24 +13,55 @@ def logout_page():
return redirect(url_for(config.ROOT_ENDPOINT))


@blueprint.route("/login")
@blueprint.route("/login", methods=['GET', 'POST'])
def login():
if request.method == "POST":
form = RegistrationForm()
form = LoginForm()

# TODO
if request.method == "POST":
if not form.validate():
flash('Form invalid')
flash('Form invalid', 'warning')
return redirect(url_for('auth.login'))

if User.auth(request.form['email'], request.form['password']):
return redirect(request.args.get("next") or url_for(config.ROOT_ENDPOINT))
else:
flash('Email or Password Incorrect', 'warning')
return redirect(url_for('auth.login'))

return render_template("auth/login.html")
return render_template("auth/login.html", form=form)

@blueprint.route("/register")

@blueprint.route("/register", methods=['GET', 'POST'])
def register():
form = RegistrationForm()

if request.method == "POST":

if not form.validate():
flash('Form invalid', 'warning')
return redirect(url_for('auth.register'))

if User.get_by_username(request.form['username']):
flash('Username is taken', 'warning')
return redirect(url_for('auth.register'))

if User.get_by_email(request.form['email']):
flash('Email is taken', 'warning')
return redirect(url_for('auth.register'))

User.create(request.form['username'], request.form['email'], request.form['password'])
User.auth(request.form['email'], request.form['password'])

return redirect(request.args.get("next") or url_for(config.ROOT_ENDPOINT))
else:
return render_template("auth/register.html")

return render_template("auth/register.html", form=form)


@blueprint.route("/settings", methods=['GET', 'POST'])
def settings():
return render_template("auth/settings.html")

@blueprint.route("/logout")
def logout():
User.logout()
return redirect("/")

+ 6
- 7
realms/modules/wiki/assets.py View File

@@ -1,8 +1,7 @@
from realms.lib.assets import register
from realms import assets

register(
'editor',
'js/ace/ace.js',
'js/ace/mode-markdown.js',
'vendor/keymaster/keymaster.js',
'js/dillinger.js')
assets.register('editor',
'js/ace/ace.js',
'js/ace/mode-markdown.js',
'vendor/keymaster/keymaster.js',
'js/dillinger.js')

realms/lib/wiki.py → realms/modules/wiki/models.py View File

@@ -7,7 +7,8 @@ import gittle.utils
from gittle import Gittle
from dulwich.repo import NotGitRepository
from werkzeug.utils import escape, unescape
from util import to_canonical
from realms.lib.util import to_canonical
from realms import cache


class MyGittle(Gittle):
@@ -94,7 +95,7 @@ class Wiki():

content = lxml.html.tostring(tree, encoding='utf-8', method='html')

# post processing to fix errors
# remove added div tags
content = content[5:-6]

# FIXME this is for block quotes, doesn't work for double ">"
@@ -103,7 +104,8 @@ class Wiki():

content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL)

filename = self.cname_to_filename(to_canonical(name))
cname = to_canonical(name)
filename = self.cname_to_filename(cname)
with open(self.path + "/" + filename, 'w') as f:
f.write(content)

@@ -119,10 +121,14 @@ class Wiki():
if not email:
email = self.default_committer_email

return self.repo.commit(name=username,
email=email,
message=message,
files=[filename])
ret = self.repo.commit(name=username,
email=email,
message=message,
files=[filename])

cache.delete_memoized(Wiki.get_page, cname)

return ret

def rename_page(self, old_name, new_name):
old_name, new_name = map(self.cname_to_filename, [old_name, new_name])
@@ -131,7 +137,10 @@ class Wiki():
email=self.default_committer_email,
message="Moving %s to %s" % (old_name, new_name),
files=[old_name])
cache.delete_memoized(Wiki.get_page, old_name)
cache.delete_memoized(Wiki.get_page, new_name)

@cache.memoize()
def get_page(self, name, sha='HEAD'):
# commit = gittle.utils.git.commit_info(self.repo[sha])
name = self.cname_to_filename(name).encode('latin-1')
@@ -151,5 +160,6 @@ class Wiki():
def get_history(self, name):
return self.repo.file_history(self.cname_to_filename(name))

def cname_to_filename(self, cname):
@staticmethod
def cname_to_filename(cname):
return cname.lower() + ".md"

+ 0
- 5
realms/modules/wiki/tests.py View File

@@ -1,5 +0,0 @@
import realms

c = realms.app.test_client()
print c.get('/wiki/_create')
print c.get('/wiki/_create/blah')

+ 33
- 26
realms/modules/wiki/views.py View File

@@ -1,15 +1,19 @@
from flask import g, render_template, request, redirect, Blueprint, flash, url_for
from flask import g, render_template, request, redirect, Blueprint, flash, url_for, current_app
from flask.ext.login import login_required
from realms.lib.util import to_canonical, remove_ext
from realms import config
from realms.modules.wiki.models import Wiki
from realms import config, current_user

blueprint = Blueprint('wiki', __name__, url_prefix=config.RELATIVE_PATH)

wiki = Wiki(config.WIKI_PATH)


@blueprint.route("/_commit/<sha>/<name>")
def commit(name, sha):
cname = to_canonical(name)

data = g.current_wiki.get_page(cname, sha=sha)
data = wiki.get_page(cname, sha=sha)
if data:
return render_template('wiki/page.html', name=name, page=data, commit=sha)
else:
@@ -18,41 +22,42 @@ def commit(name, sha):

@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)
diff = wiki.compare(name, fsha, lsha)
return render_template('wiki/compare.html', name=name, diff=diff, old=fsha, new=lsha)


@blueprint.route("/_revert", methods=['POST'])
@login_required
def revert():
if request.method == 'POST':
name = request.form.get('name')
commit = request.form.get('commit')
cname = to_canonical(name)
g.current_wiki.revert_page(name, commit, message="Reverting %s" % cname,
username=g.current_user.get('username'))
flash('Page reverted', 'success')
return redirect(url_for('wiki.page', name=cname))
name = request.form.get('name')
commit = request.form.get('commit')
cname = to_canonical(name)
wiki.revert_page(name, commit, message="Reverting %s" % cname,
username=g.current_user.username)
flash('Page reverted', 'success')
return redirect(url_for('wiki.page', name=cname))


@blueprint.route("/_history/<name>")
def history(name):
history = g.current_wiki.get_history(name)
history = wiki.get_history(name)
return render_template('wiki/history.html', name=name, history=history, wiki_home=url_for('wiki.page'))


@blueprint.route("/_edit/<name>", methods=['GET', 'POST'])
@login_required
def edit(name):
data = g.current_wiki.get_page(name)
data = wiki.get_page(name)
cname = to_canonical(name)
if request.method == 'POST':
edit_cname = to_canonical(request.form['name'])
if edit_cname.lower() != cname.lower():
g.current_wiki.rename_page(cname, edit_cname)
wiki.rename_page(cname, edit_cname)

g.current_wiki.write_page(edit_cname,
request.form['content'],
message=request.form['message'],
username=g.current_user.get('username'))
wiki.write_page(edit_cname,
request.form['content'],
message=request.form['message'],
username=g.current_user.username)
else:
if data:
name = remove_ext(data['name'])
@@ -64,22 +69,24 @@ def edit(name):


@blueprint.route("/_delete/<name>", methods=['POST'])
@login_required
def delete(name):
pass


@blueprint.route("/_create/", defaults={'name': None}, methods=['GET', 'POST'])
@blueprint.route("/_create/<name>", methods=['GET', 'POST'])
@login_required
def create(name):
if request.method == 'POST':
g.current_wiki.write_page(request.form['name'],
request.form['content'],
message=request.form['message'],
create=True,
username=g.current_user.get('username'))
wiki.write_page(request.form['name'],
request.form['content'],
message=request.form['message'],
create=True,
username=g.current_user.username)
else:
cname = to_canonical(name) if name else ""
if cname and g.current_wiki.get_page(cname):
if cname and wiki.get_page(cname):
# Page exists, edit instead
return redirect(url_for('wiki.edit', name=cname))

@@ -94,7 +101,7 @@ def page(name):
if cname != name:
return redirect(url_for('wiki.page', name=cname))

data = g.current_wiki.get_page(cname)
data = wiki.get_page(cname)

if data:
return render_template('wiki/page.html', name=cname, page=data)


+ 39
- 8
realms/static/css/style.css View File

@@ -145,14 +145,8 @@ body {
border-radius: 2px;
}


.navbar-nav .user-avatar a {

}

.navbar-nav>li.user-avatar a {
padding-top: 9px;
padding-bottom: 9px;
.navbar-nav>li.user-avatar img {
margin-right: 3px;
}

.floating-header {
@@ -169,4 +163,41 @@ body {
/* 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;
}

input.parsley-success,
select.parsley-success,
textarea.parsley-success {
color: #468847;
background-color: #DFF0D8;
border: 1px solid #D6E9C6;
}

input.parsley-error,
select.parsley-error,
textarea.parsley-error {
color: #B94A48;
background-color: #F2DEDE;
border: 1px solid #EED3D7;
}

.parsley-errors-list {
margin: 2px 0 3px 0;
padding: 0;
list-style-type: none;
font-size: 0.9em;
line-height: 0.9em;
opacity: 0;
-moz-opacity: 0;
-webkit-opacity: 0;

transition: all .3s ease-in;
-o-transition: all .3s ease-in;
-ms-transition: all .3s ease-in;
-moz-transition: all .3s ease-in;
-webkit-transition: all .3s ease-in;
}

.parsley-errors-list.filled {
opacity: 1;
}

+ 6
- 16
realms/templates/auth/login.html View File

@@ -1,18 +1,8 @@
{% extends 'layout.html' %}
{% from 'macros.html' import render_form, render_field %}
{% block body %}

<form role="form" method="post" action="{{ url_for('auth.login') }}" data-parsley-validate>
<div class="form-group">
<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" placeholder="Password" min="6" required />
</div>

<input type="submit" class="btn btn-primary" value="Login" />
</form>

{% endblock %}
{% call render_form(form, action_url=url_for('auth.login'), action_text='Login', btn_class='btn btn-primary') %}
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
{% endcall %}
{% endblock %}

+ 10
- 26
realms/templates/auth/register.html View File

@@ -1,29 +1,13 @@
{% extends 'layout.html' %}
{% from 'macros.html' import render_form, render_field %}
{% block body %}

<form role="form" method="post" action="{{ url_for('auth.register') }}" data-parsley-validate>
<div class="form-group">
<label for="username">Username</label>
<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 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" placeholder="Password" required min="6"/>
</div>

<div class="form-group">
<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="Register" />
</form>

{% call render_form(form, action_url=url_for('auth.register'), action_text='Register', btn_class='btn btn-primary') %}
{{ render_field(form.username, placeholder='Username', type='username', **{"required": 1, "data-parsley-type": "alphanum"}) }}
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
{{ render_field(form.password, placeholder='Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }}
{{ render_field(form.confirm, placeholder='Confirm Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }}
{% if config.RECAPTCHA_ENABLE %}
{{ render_field(form.recaptcha) }}
{% endif %}
{% endcall %}
{% endblock %}

+ 5
- 0
realms/templates/auth/settings.html View File

@@ -0,0 +1,5 @@
{% extends 'layout.html' %}
{% from 'macros.html' import render_form, render_field %}
{% block body %}

{% endblock %}

+ 4
- 6
realms/templates/layout.html View File

@@ -43,16 +43,16 @@
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
{% if session.get('user') %}
{% if g.current_user.is_authenticated() %}
<li class="dropdown user-avatar">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span>
<img src="{{ session['user'].get('avatar') }}" class="menu-avatar">
<span>{{ session['user'].get('username') }} <i class="icon-caret-down"></i></span>
<img src="{{ g.current_user.avatar }}" class="menu-avatar">
<span>{{ g.current_user.username }} <i class="icon-caret-down"></i></span>
</span>
</a>
<ul class="dropdown-menu">
<li><a href="{{ url_for('account') }}">Account</a></li>
<li><a href="{{ url_for('auth.settings') }}">Settings</a></li>
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
</ul>
</li>
@@ -68,8 +68,6 @@
<!-- Page Menu -->




<div class="container">
<div id="main-body">
{% with messages = get_flashed_messages(with_categories=True) %}


+ 99
- 0
realms/templates/macros.html View File

@@ -0,0 +1,99 @@
{# Source: https://gist.github.com/bearz/7394681 #}

{# Renders field for bootstrap 3 standards.

Params:
field - WTForm field
kwargs - pass any arguments you want in order to put them into the html attributes.
There are few exceptions: for - for_, class - class_, class__ - class_

Example usage:
{{ macros.render_field(form.email, placeholder='Input email', type='email') }}
#}
{% macro render_field(field, label_visible=true) -%}

<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
{% if (field.type != 'HiddenField' or field.type !='CSRFTokenField') and label_visible %}
<label for="{{ field.id }}" class="control-label">{{ field.label }}</label>
{% endif %}
{{ field(class_='form-control', **kwargs) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
</div>
{%- endmacro %}

{# Renders checkbox fields since they are represented differently in bootstrap
Params:
field - WTForm field (there are no check, but you should put here only BooleanField.
kwargs - pass any arguments you want in order to put them into the html attributes.
There are few exceptions: for - for_, class - class_, class__ - class_

Example usage:
{{ macros.render_checkbox_field(form.remember_me) }}
#}
{% macro render_checkbox_field(field) -%}
<div class="checkbox">
<label>
{{ field(type='checkbox', **kwargs) }} {{ field.label }}
</label>
</div>
{%- endmacro %}

{# Renders radio field
Params:
field - WTForm field (there are no check, but you should put here only BooleanField.
kwargs - pass any arguments you want in order to put them into the html attributes.
There are few exceptions: for - for_, class - class_, class__ - class_

Example usage:
{{ macros.render_radio_field(form.answers) }}
#}
{% macro render_radio_field(field) -%}
{% for value, label, _ in field.iter_choices() %}
<div class="radio">
<label>
<input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}">{{ label }}
</label>
</div>
{% endfor %}
{%- endmacro %}

{# Renders WTForm in bootstrap way. There are two ways to call function:
- as macros: it will render all field forms using cycle to iterate over them
- as call: it will insert form fields as you specify:
e.g. {% call macros.render_form(form, action_url=url_for('login_view'), action_text='Login',
class_='login-form') %}
{{ macros.render_field(form.email, placeholder='Input email', type='email') }}
{{ macros.render_field(form.password, placeholder='Input password', type='password') }}
{{ macros.render_checkbox_field(form.remember_me, type='checkbox') }}
{% endcall %}

Params:
form - WTForm class
action_url - url where to submit this form
action_text - text of submit button
class_ - sets a class for form
#}
{% macro render_form(form, action_url='', action_text='Submit', class_='', btn_class='btn btn-default') -%}

<form method="POST" action="{{ action_url }}" role="form" class="{{ class_ }}" data-parsley-validate>
{{ form.hidden_tag() if form.hidden_tag }}
{% if caller %}
{{ caller() }}
{% else %}
{% for f in form %}
{% if f.type == 'BooleanField' %}
{{ render_checkbox_field(f) }}
{% elif f.type == 'RadioField' %}
{{ render_radio_field(f) }}
{% else %}
{{ render_field(f) }}
{% endif %}
{% endfor %}
{% endif %}
<button type="submit" class="{{ btn_class }}">{{ action_text }} </button>
</form>
{%- endmacro %}

+ 3
- 1
requirements.txt View File

@@ -1,8 +1,10 @@
bcrypt
Flask
Flask-Login
Flask-Assets
Flask-Cache
Flask-Login
Flask-Script
Flask-SQLAlchemy
Flask-WTF
beautifulsoup4
closure==20121212


Loading…
Cancel
Save