WIP
This commit is contained in:
parent
b02d3db684
commit
86f0549e44
1
Vagrantfile
vendored
1
Vagrantfile
vendored
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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,16 +157,49 @@ def error_handler(e):
|
|||
|
||||
return response, status_code
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Application(__name__)
|
||||
app.config.from_object('realms.config')
|
||||
app.url_map.converters['regex'] = RegexConverter
|
||||
app.url_map.strict_slashes = False
|
||||
|
||||
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
|
||||
@app.before_request
|
||||
def init_g():
|
||||
g.assets = ['main']
|
||||
|
||||
register('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
|
||||
|
||||
if config.RELATIVE_PATH:
|
||||
@app.route("/")
|
||||
def root():
|
||||
return redirect(url_for(config.ROOT_ENDPOINT))
|
||||
|
||||
return app
|
||||
|
||||
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',
|
||||
|
@ -180,43 +207,17 @@ register('main',
|
|||
'vendor/showdown/src/showdown.js',
|
||||
'vendor/showdown/src/extensions/table.js',
|
||||
'js/wmd.js',
|
||||
'js/html-sanitizer-minified.js', # don't minify
|
||||
'js/html-sanitizer-minified.js', # don't minify?
|
||||
'vendor/highlightjs/highlight.pack.js',
|
||||
'vendor/parsleyjs/dist/parsley.js',
|
||||
'js/main.js')
|
||||
|
||||
|
||||
@app.before_request
|
||||
def init_g():
|
||||
g.assets = ['main']
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
if config.RELATIVE_PATH:
|
||||
@app.route("/")
|
||||
def root():
|
||||
return redirect(url_for(config.ROOT_ENDPOINT))
|
||||
|
||||
|
||||
app.discover()
|
||||
|
||||
db.create_all()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
SECRET = 'K3dRq1q9eN72GJDkgvyshFVwlqHHCyPI'
|
||||
CACHE_TYPE = 'simple'
|
||||
|
||||
# Redis Example
|
||||
"""
|
||||
CACHE_TYPE = 'redis'
|
||||
CACHE_REDIS_HOST = '127.0.0.1'
|
||||
CACHE_REDIS_PORT = 6379
|
||||
CACHE_REDIS_DB = '0'
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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
realms/lib/model.py
Normal file
287
realms/lib/model.py
Normal 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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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()])
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
def __init__(self, email, data=None):
|
||||
self.id = email
|
||||
for k, v in data.items():
|
||||
setattr(self, FIELD_MAP.get(k, k), v)
|
||||
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)
|
||||
|
||||
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 get_by_username(username):
|
||||
return User.query.filter_by(username=username).first()
|
||||
|
||||
@staticmethod
|
||||
def get_by_email(email):
|
||||
return User.query.filter_by(email=email).first()
|
||||
|
||||
@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
|
||||
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
|
|
@ -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))
|
||||
|
||||
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/register.html")
|
||||
flash('Email or Password Incorrect', 'warning')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
return render_template("auth/login.html", form=form)
|
||||
|
||||
|
||||
@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))
|
||||
|
||||
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("/")
|
|
@ -1,7 +1,6 @@
|
|||
from realms.lib.assets import register
|
||||
from realms import assets
|
||||
|
||||
register(
|
||||
'editor',
|
||||
assets.register('editor',
|
||||
'js/ace/ace.js',
|
||||
'js/ace/mode-markdown.js',
|
||||
'vendor/keymaster/keymaster.js',
|
||||
|
|
|
@ -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,11 +121,15 @@ class Wiki():
|
|||
if not email:
|
||||
email = self.default_committer_email
|
||||
|
||||
return self.repo.commit(name=username,
|
||||
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])
|
||||
self.repo.mv([(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"
|
|
@ -1,5 +0,0 @@
|
|||
import realms
|
||||
|
||||
c = realms.app.test_client()
|
||||
print c.get('/wiki/_create')
|
||||
print c.get('/wiki/_create/blah')
|
|
@ -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'))
|
||||
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,
|
||||
wiki.write_page(edit_cname,
|
||||
request.form['content'],
|
||||
message=request.form['message'],
|
||||
username=g.current_user.get('username'))
|
||||
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'],
|
||||
wiki.write_page(request.form['name'],
|
||||
request.form['content'],
|
||||
message=request.form['message'],
|
||||
create=True,
|
||||
username=g.current_user.get('username'))
|
||||
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)
|
||||
|
|
|
@ -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 {
|
||||
|
@ -170,3 +164,40 @@ body {
|
|||
/* 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;
|
||||
}
|
|
@ -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>
|
||||
|
||||
{% 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 %}
|
|
@ -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
realms/templates/auth/settings.html
Normal file
5
realms/templates/auth/settings.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% from 'macros.html' import render_form, render_field %}
|
||||
{% block body %}
|
||||
|
||||
{% endblock %}
|
|
@ -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
realms/templates/macros.html
Normal file
99
realms/templates/macros.html
Normal 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 %}
|
|
@ -1,8 +1,10 @@
|
|||
bcrypt
|
||||
Flask
|
||||
Flask-Login
|
||||
Flask-Assets
|
||||
Flask-Cache
|
||||
Flask-Login
|
||||
Flask-Script
|
||||
Flask-SQLAlchemy
|
||||
Flask-WTF
|
||||
beautifulsoup4
|
||||
closure==20121212
|
||||
|
|
Loading…
Reference in a new issue