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 "srv/", "/srv/"
|
||||||
config.vm.synced_folder ".", "/home/deploy/realms"
|
config.vm.synced_folder ".", "/home/deploy/realms"
|
||||||
|
config.vm.synced_folder "~/.virtualenvs", "/home/deploy/virtualenvs"
|
||||||
config.vm.provision :salt do |salt|
|
config.vm.provision :salt do |salt|
|
||||||
salt.minion_config = "srv/minion"
|
salt.minion_config = "srv/minion"
|
||||||
salt.run_highstate = true
|
salt.run_highstate = true
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
from gevent import wsgi
|
from gevent import wsgi
|
||||||
from realms import config, app, manager
|
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
|
@manager.command
|
||||||
def server():
|
def run():
|
||||||
|
"""
|
||||||
|
Run production ready server
|
||||||
|
"""
|
||||||
print "Server started. Env: %s Port: %s" % (config.ENV, config.PORT)
|
print "Server started. Env: %s Port: %s" % (config.ENV, config.PORT)
|
||||||
wsgi.WSGIServer(('', int(config.PORT)), app).serve_forever()
|
wsgi.WSGIServer(('', int(config.PORT)), app).serve_forever()
|
||||||
|
|
||||||
|
|
|
@ -20,18 +20,18 @@ import sys
|
||||||
import json
|
import json
|
||||||
import httplib
|
import httplib
|
||||||
import traceback
|
import traceback
|
||||||
from flask import Flask, request, render_template, url_for, redirect, session, flash, g
|
from flask import Flask, request, render_template, url_for, redirect, g
|
||||||
from flask.ctx import _AppCtxGlobals
|
from flask.ctx import _AppCtxGlobals
|
||||||
|
from flask.ext.cache import Cache
|
||||||
from flask.ext.script import Manager
|
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.routing import BaseConverter
|
||||||
from werkzeug.utils import cached_property
|
from werkzeug.utils import cached_property
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from realms import config
|
from realms import config
|
||||||
from realms.lib.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.lib.util import to_canonical, remove_ext, mkdir_safe, gravatar_url, to_dict
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,11 +39,7 @@ class AppCtxGlobals(_AppCtxGlobals):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def current_user(self):
|
def current_user(self):
|
||||||
return session.get('user') if session.get('user') else {'username': 'Anon'}
|
return current_user
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def current_wiki(self):
|
|
||||||
return Wiki(config.WIKI_PATH)
|
|
||||||
|
|
||||||
|
|
||||||
class Application(Flask):
|
class Application(Flask):
|
||||||
|
@ -68,8 +64,8 @@ class Application(Flask):
|
||||||
return super(Application, self).__call__(environ, start_response)
|
return super(Application, self).__call__(environ, start_response)
|
||||||
|
|
||||||
def discover(self):
|
def discover(self):
|
||||||
IMPORT_NAME = 'realms.modules'
|
import_name = 'realms.modules'
|
||||||
FROMLIST = (
|
fromlist = (
|
||||||
'assets',
|
'assets',
|
||||||
'commands',
|
'commands',
|
||||||
'models',
|
'models',
|
||||||
|
@ -78,10 +74,10 @@ class Application(Flask):
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
__import__(IMPORT_NAME, fromlist=FROMLIST)
|
__import__(import_name, fromlist=fromlist)
|
||||||
|
|
||||||
for module_name in self.config['MODULES']:
|
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
|
# Blueprint
|
||||||
if hasattr(sources, 'views'):
|
if hasattr(sources, 'views'):
|
||||||
|
@ -107,6 +103,18 @@ class Application(Flask):
|
||||||
return super(Application, self).make_response(tuple(rv))
|
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):
|
class RegexConverter(BaseConverter):
|
||||||
"""
|
"""
|
||||||
Enables Regex matching on endpoints
|
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')
|
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):
|
def error_handler(e):
|
||||||
try:
|
try:
|
||||||
if isinstance(e, HTTPException):
|
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']:
|
if request.is_xhr or request.accept_mimetypes.best in ['application/json', 'text/javascript']:
|
||||||
response = {
|
response = {
|
||||||
'message': message,
|
'message': message,
|
||||||
'traceback': tb,
|
'traceback': tb
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
response = render_template('errors/error.html',
|
response = render_template('errors/error.html',
|
||||||
|
@ -163,60 +157,67 @@ def error_handler(e):
|
||||||
|
|
||||||
return response, status_code
|
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
|
def create_app():
|
||||||
assets.init_app(app)
|
app = Application(__name__)
|
||||||
assets.app = app
|
app.config.from_object('realms.config')
|
||||||
assets.debug = config.DEBUG
|
app.url_map.converters['regex'] = RegexConverter
|
||||||
|
app.url_map.strict_slashes = False
|
||||||
|
|
||||||
register('main',
|
for status_code in httplib.responses:
|
||||||
'vendor/jquery/jquery.js',
|
if status_code >= 400:
|
||||||
'vendor/components-underscore/underscore.js',
|
app.register_error_handler(status_code, error_handler)
|
||||||
'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.before_request
|
||||||
|
def init_g():
|
||||||
|
g.assets = ['main']
|
||||||
|
|
||||||
@app.before_request
|
@app.template_filter('datetime')
|
||||||
def init_g():
|
def _jinja2_filter_datetime(ts):
|
||||||
g.assets = ['main']
|
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
|
if config.RELATIVE_PATH:
|
||||||
def inject_x_rate_headers(response):
|
@app.route("/")
|
||||||
limit = get_view_rate_limit()
|
def root():
|
||||||
if limit and limit.send_x_headers:
|
return redirect(url_for(config.ROOT_ENDPOINT))
|
||||||
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
|
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
@app.template_filter('datetime')
|
app = create_app()
|
||||||
def _jinja2_filter_datetime(ts):
|
|
||||||
return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts))
|
|
||||||
|
|
||||||
|
# Init plugins here if possible
|
||||||
|
manager = Manager(app)
|
||||||
|
|
||||||
@app.errorhandler(404)
|
login_manager = LoginManager(app)
|
||||||
def page_not_found(e):
|
login_manager.login_view = 'auth.login'
|
||||||
return render_template('errors/404.html'), 404
|
|
||||||
|
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
cache = Cache(app)
|
||||||
|
|
||||||
if config.RELATIVE_PATH:
|
assets = Environment(app)
|
||||||
@app.route("/")
|
assets.register('main',
|
||||||
def root():
|
'vendor/jquery/jquery.js',
|
||||||
return redirect(url_for(config.ROOT_ENDPOINT))
|
'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()
|
app.discover()
|
||||||
|
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,19 +7,35 @@ ENV = 'DEV'
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
ASSETS_DEBUG = True
|
ASSETS_DEBUG = True
|
||||||
|
|
||||||
|
SQLALCHEMY_ECHO = True
|
||||||
|
|
||||||
PORT = 80
|
PORT = 80
|
||||||
BASE_URL = 'http://realms.dev'
|
BASE_URL = 'http://realms.dev'
|
||||||
|
|
||||||
REDIS_HOST = '127.0.0.1'
|
DB_URI = 'sqlite:////home/deploy/wiki.db'
|
||||||
REDIS_PORT = 6379
|
|
||||||
REDIS_DB = '0'
|
|
||||||
|
|
||||||
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_PATH = '/home/deploy/wiki'
|
||||||
WIKI_HOME = 'home'
|
WIKI_HOME = 'home'
|
||||||
|
|
||||||
ALLOW_ANON = True
|
ALLOW_ANON = True
|
||||||
|
LOGIN_DISABLED = ALLOW_ANON
|
||||||
|
|
||||||
ROOT_ENDPOINT = 'wiki.page'
|
ROOT_ENDPOINT = 'wiki.page'
|
||||||
|
|
||||||
|
@ -27,10 +43,11 @@ with open(os.path.join(os.path.dirname(__file__) + "/../../", 'config.json')) as
|
||||||
__settings = json.load(f)
|
__settings = json.load(f)
|
||||||
globals().update(__settings)
|
globals().update(__settings)
|
||||||
|
|
||||||
# String trailing slash
|
|
||||||
if BASE_URL.endswith('/'):
|
if BASE_URL.endswith('/'):
|
||||||
BASE_URL = BASE_URL[-1]
|
BASE_URL = BASE_URL[-1]
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URI = DB_URI
|
||||||
|
|
||||||
_url = urlparse(BASE_URL)
|
_url = urlparse(BASE_URL)
|
||||||
RELATIVE_PATH = _url.path
|
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 hashlib
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from realms.lib.services import db
|
|
||||||
|
|
||||||
|
|
||||||
class AttrDict(dict):
|
class AttrDict(dict):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -34,36 +32,6 @@ def to_dict(data):
|
||||||
return row2dict(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):
|
def mkdir_safe(path):
|
||||||
if path and not(os.path.exists(path)):
|
if path and not(os.path.exists(path)):
|
||||||
os.makedirs(path)
|
os.makedirs(path)
|
||||||
|
|
|
@ -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):
|
class RegistrationForm(Form):
|
||||||
username = StringField('Username', [validators.Length(min=4, max=25)])
|
username = StringField('Username', [validators.Length(min=4, max=25)])
|
||||||
|
@ -10,9 +11,12 @@ class RegistrationForm(Form):
|
||||||
])
|
])
|
||||||
confirm = PasswordField('Repeat Password')
|
confirm = PasswordField('Repeat Password')
|
||||||
|
|
||||||
|
if config.RECAPTCHA_ENABLE:
|
||||||
|
setattr(RegistrationForm, 'recaptcha', RecaptchaField("You Human?"))
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(Form):
|
class LoginForm(Form):
|
||||||
email = StringField('Email', [validators.DataRequired])
|
email = StringField('Email', [validators.DataRequired()])
|
||||||
password = PasswordField('Password', [validators.DataRequired])
|
password = PasswordField('Password', [validators.DataRequired()])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,15 @@
|
||||||
from flask.ext.login import UserMixin, logout_user, login_user
|
from flask.ext.login import UserMixin, logout_user, login_user, AnonymousUserMixin
|
||||||
from realms import config, login_manager
|
from realms import config, login_manager, db
|
||||||
from realms.lib.services import db
|
from realms.lib.model import Model
|
||||||
|
from realms.lib.util import gravatar_url
|
||||||
from itsdangerous import URLSafeSerializer, BadSignature
|
from itsdangerous import URLSafeSerializer, BadSignature
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
import json
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|
||||||
FIELD_MAP = dict(
|
|
||||||
u='username',
|
|
||||||
e='email',
|
|
||||||
p='password',
|
|
||||||
nv='not_verified',
|
|
||||||
a='admin',
|
|
||||||
b='banned')
|
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
return User.get(user_id)
|
return User.get_by_id(user_id)
|
||||||
|
|
||||||
|
|
||||||
@login_manager.token_loader
|
@login_manager.token_loader
|
||||||
|
@ -29,7 +21,7 @@ def load_token(token):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# User key *could* be stored in payload to avoid user lookup in db
|
# 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:
|
if not user:
|
||||||
return False
|
return False
|
||||||
|
@ -43,68 +35,78 @@ def load_token(token):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin):
|
class AnonUser(AnonymousUserMixin):
|
||||||
|
username = 'Anon'
|
||||||
|
email = ''
|
||||||
|
|
||||||
username = None
|
|
||||||
email = None
|
|
||||||
password = None
|
|
||||||
|
|
||||||
def __init__(self, email, data=None):
|
class User(Model, UserMixin):
|
||||||
self.id = email
|
__tablename__ = 'users'
|
||||||
for k, v in data.items():
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
setattr(self, FIELD_MAP.get(k, k), v)
|
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):
|
def get_auth_token(self):
|
||||||
key = sha256(self.password).hexdigest()
|
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
|
@staticmethod
|
||||||
def create(username, email, password):
|
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
|
@staticmethod
|
||||||
def signer(salt):
|
def signer(salt):
|
||||||
"""
|
"""
|
||||||
Signed with app secret salted with sha256 of password hash of user (client secret)
|
Signed with app secret salted with sha256 of password hash of user (client secret)
|
||||||
"""
|
"""
|
||||||
return URLSafeSerializer(config.SECRET + salt)
|
return URLSafeSerializer(config.SECRET_KEY + 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
|
@staticmethod
|
||||||
def auth(email, password):
|
def auth(email, password):
|
||||||
user = User.get(email)
|
user = User.query.filter_by(email=email).first()
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
|
# User doesn't exist
|
||||||
return False
|
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)
|
login_user(user, remember=True)
|
||||||
return user
|
return user
|
||||||
else:
|
else:
|
||||||
|
# Password check failed
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def hash(password):
|
def hash_password(password):
|
||||||
return bcrypt.hashpw(password, bcrypt.gensalt(log_rounds=12))
|
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
|
@classmethod
|
||||||
def logout(cls):
|
def logout(cls):
|
||||||
logout_user()
|
logout_user()
|
||||||
|
|
||||||
|
login_manager.anonymous_user = AnonUser
|
|
@ -13,24 +13,55 @@ def logout_page():
|
||||||
return redirect(url_for(config.ROOT_ENDPOINT))
|
return redirect(url_for(config.ROOT_ENDPOINT))
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/login")
|
@blueprint.route("/login", methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
if request.method == "POST":
|
form = LoginForm()
|
||||||
form = RegistrationForm()
|
|
||||||
|
|
||||||
# TODO
|
if request.method == "POST":
|
||||||
if not form.validate():
|
if not form.validate():
|
||||||
flash('Form invalid')
|
flash('Form invalid', 'warning')
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
if User.auth(request.form['email'], request.form['password']):
|
if User.auth(request.form['email'], request.form['password']):
|
||||||
return redirect(request.args.get("next") or url_for(config.ROOT_ENDPOINT))
|
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():
|
def register():
|
||||||
|
form = RegistrationForm()
|
||||||
|
|
||||||
if request.method == "POST":
|
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 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("/")
|
|
@ -1,8 +1,7 @@
|
||||||
from realms.lib.assets import register
|
from realms import assets
|
||||||
|
|
||||||
register(
|
assets.register('editor',
|
||||||
'editor',
|
'js/ace/ace.js',
|
||||||
'js/ace/ace.js',
|
'js/ace/mode-markdown.js',
|
||||||
'js/ace/mode-markdown.js',
|
'vendor/keymaster/keymaster.js',
|
||||||
'vendor/keymaster/keymaster.js',
|
'js/dillinger.js')
|
||||||
'js/dillinger.js')
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ import gittle.utils
|
||||||
from gittle import Gittle
|
from gittle import Gittle
|
||||||
from dulwich.repo import NotGitRepository
|
from dulwich.repo import NotGitRepository
|
||||||
from werkzeug.utils import escape, unescape
|
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):
|
class MyGittle(Gittle):
|
||||||
|
@ -94,7 +95,7 @@ class Wiki():
|
||||||
|
|
||||||
content = lxml.html.tostring(tree, encoding='utf-8', method='html')
|
content = lxml.html.tostring(tree, encoding='utf-8', method='html')
|
||||||
|
|
||||||
# post processing to fix errors
|
# remove added div tags
|
||||||
content = content[5:-6]
|
content = content[5:-6]
|
||||||
|
|
||||||
# FIXME this is for block quotes, doesn't work for double ">"
|
# 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)
|
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:
|
with open(self.path + "/" + filename, 'w') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
@ -119,10 +121,14 @@ class Wiki():
|
||||||
if not email:
|
if not email:
|
||||||
email = self.default_committer_email
|
email = self.default_committer_email
|
||||||
|
|
||||||
return self.repo.commit(name=username,
|
ret = self.repo.commit(name=username,
|
||||||
email=email,
|
email=email,
|
||||||
message=message,
|
message=message,
|
||||||
files=[filename])
|
files=[filename])
|
||||||
|
|
||||||
|
cache.delete_memoized(Wiki.get_page, cname)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
def rename_page(self, old_name, new_name):
|
def rename_page(self, old_name, new_name):
|
||||||
old_name, new_name = map(self.cname_to_filename, [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,
|
email=self.default_committer_email,
|
||||||
message="Moving %s to %s" % (old_name, new_name),
|
message="Moving %s to %s" % (old_name, new_name),
|
||||||
files=[old_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'):
|
def get_page(self, name, sha='HEAD'):
|
||||||
# commit = gittle.utils.git.commit_info(self.repo[sha])
|
# commit = gittle.utils.git.commit_info(self.repo[sha])
|
||||||
name = self.cname_to_filename(name).encode('latin-1')
|
name = self.cname_to_filename(name).encode('latin-1')
|
||||||
|
@ -151,5 +160,6 @@ class Wiki():
|
||||||
def get_history(self, name):
|
def get_history(self, name):
|
||||||
return self.repo.file_history(self.cname_to_filename(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"
|
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.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)
|
blueprint = Blueprint('wiki', __name__, url_prefix=config.RELATIVE_PATH)
|
||||||
|
|
||||||
|
wiki = Wiki(config.WIKI_PATH)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/_commit/<sha>/<name>")
|
@blueprint.route("/_commit/<sha>/<name>")
|
||||||
def commit(name, sha):
|
def commit(name, sha):
|
||||||
cname = to_canonical(name)
|
cname = to_canonical(name)
|
||||||
|
|
||||||
data = g.current_wiki.get_page(cname, sha=sha)
|
data = wiki.get_page(cname, sha=sha)
|
||||||
if data:
|
if data:
|
||||||
return render_template('wiki/page.html', name=name, page=data, commit=sha)
|
return render_template('wiki/page.html', name=name, page=data, commit=sha)
|
||||||
else:
|
else:
|
||||||
|
@ -18,41 +22,42 @@ def commit(name, sha):
|
||||||
|
|
||||||
@blueprint.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
|
@blueprint.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
|
||||||
def compare(name, fsha, dots, lsha):
|
def compare(name, fsha, dots, lsha):
|
||||||
diff = g.current_wiki.compare(name, fsha, lsha)
|
diff = wiki.compare(name, fsha, lsha)
|
||||||
return render_template('wiki/compare.html', name=name, diff=diff, old=fsha, new=lsha)
|
return render_template('wiki/compare.html', name=name, diff=diff, old=fsha, new=lsha)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/_revert", methods=['POST'])
|
@blueprint.route("/_revert", methods=['POST'])
|
||||||
|
@login_required
|
||||||
def revert():
|
def revert():
|
||||||
if request.method == 'POST':
|
name = request.form.get('name')
|
||||||
name = request.form.get('name')
|
commit = request.form.get('commit')
|
||||||
commit = request.form.get('commit')
|
cname = to_canonical(name)
|
||||||
cname = to_canonical(name)
|
wiki.revert_page(name, commit, message="Reverting %s" % cname,
|
||||||
g.current_wiki.revert_page(name, commit, message="Reverting %s" % cname,
|
username=g.current_user.username)
|
||||||
username=g.current_user.get('username'))
|
flash('Page reverted', 'success')
|
||||||
flash('Page reverted', 'success')
|
return redirect(url_for('wiki.page', name=cname))
|
||||||
return redirect(url_for('wiki.page', name=cname))
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/_history/<name>")
|
@blueprint.route("/_history/<name>")
|
||||||
def 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'))
|
return render_template('wiki/history.html', name=name, history=history, wiki_home=url_for('wiki.page'))
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/_edit/<name>", methods=['GET', 'POST'])
|
@blueprint.route("/_edit/<name>", methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
def edit(name):
|
def edit(name):
|
||||||
data = g.current_wiki.get_page(name)
|
data = wiki.get_page(name)
|
||||||
cname = to_canonical(name)
|
cname = to_canonical(name)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
edit_cname = to_canonical(request.form['name'])
|
edit_cname = to_canonical(request.form['name'])
|
||||||
if edit_cname.lower() != cname.lower():
|
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'],
|
request.form['content'],
|
||||||
message=request.form['message'],
|
message=request.form['message'],
|
||||||
username=g.current_user.get('username'))
|
username=g.current_user.username)
|
||||||
else:
|
else:
|
||||||
if data:
|
if data:
|
||||||
name = remove_ext(data['name'])
|
name = remove_ext(data['name'])
|
||||||
|
@ -64,22 +69,24 @@ def edit(name):
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/_delete/<name>", methods=['POST'])
|
@blueprint.route("/_delete/<name>", methods=['POST'])
|
||||||
|
@login_required
|
||||||
def delete(name):
|
def delete(name):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/_create/", defaults={'name': None}, methods=['GET', 'POST'])
|
@blueprint.route("/_create/", defaults={'name': None}, methods=['GET', 'POST'])
|
||||||
@blueprint.route("/_create/<name>", methods=['GET', 'POST'])
|
@blueprint.route("/_create/<name>", methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
def create(name):
|
def create(name):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
g.current_wiki.write_page(request.form['name'],
|
wiki.write_page(request.form['name'],
|
||||||
request.form['content'],
|
request.form['content'],
|
||||||
message=request.form['message'],
|
message=request.form['message'],
|
||||||
create=True,
|
create=True,
|
||||||
username=g.current_user.get('username'))
|
username=g.current_user.username)
|
||||||
else:
|
else:
|
||||||
cname = to_canonical(name) if name 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
|
# Page exists, edit instead
|
||||||
return redirect(url_for('wiki.edit', name=cname))
|
return redirect(url_for('wiki.edit', name=cname))
|
||||||
|
|
||||||
|
@ -94,7 +101,7 @@ def page(name):
|
||||||
if cname != name:
|
if cname != name:
|
||||||
return redirect(url_for('wiki.page', name=cname))
|
return redirect(url_for('wiki.page', name=cname))
|
||||||
|
|
||||||
data = g.current_wiki.get_page(cname)
|
data = wiki.get_page(cname)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
return render_template('wiki/page.html', name=cname, page=data)
|
return render_template('wiki/page.html', name=cname, page=data)
|
||||||
|
|
|
@ -145,14 +145,8 @@ body {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-nav>li.user-avatar img {
|
||||||
.navbar-nav .user-avatar a {
|
margin-right: 3px;
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-nav>li.user-avatar a {
|
|
||||||
padding-top: 9px;
|
|
||||||
padding-bottom: 9px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-header {
|
.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: -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%); */
|
/* background-image: linear-gradient(to bottom,#fff 0%,#fff 25%,rgba(255,255,255,0.9) 100%); */
|
||||||
font-size: 10px;
|
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' %}
|
{% extends 'layout.html' %}
|
||||||
|
{% from 'macros.html' import render_form, render_field %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
{% call render_form(form, action_url=url_for('auth.login'), action_text='Login', btn_class='btn btn-primary') %}
|
||||||
<form role="form" method="post" action="{{ url_for('auth.login') }}" data-parsley-validate>
|
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
|
||||||
<div class="form-group">
|
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
|
||||||
<label for="email">Email</label>
|
{% endcall %}
|
||||||
<input id="email" type="email" class="form-control" name="email" placeholder="Email" required />
|
{% endblock %}
|
||||||
</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 %}
|
|
||||||
|
|
|
@ -1,29 +1,13 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
|
{% from 'macros.html' import render_form, render_field %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
{% call render_form(form, action_url=url_for('auth.register'), action_text='Register', btn_class='btn btn-primary') %}
|
||||||
<form role="form" method="post" action="{{ url_for('auth.register') }}" data-parsley-validate>
|
{{ render_field(form.username, placeholder='Username', type='username', **{"required": 1, "data-parsley-type": "alphanum"}) }}
|
||||||
<div class="form-group">
|
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
|
||||||
<label for="username">Username</label>
|
{{ render_field(form.password, placeholder='Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }}
|
||||||
<input id="username" type="text" class="form-control" name="username" placeholder="Username" required data-parsley-type="alphanum" />
|
{{ render_field(form.confirm, placeholder='Confirm Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }}
|
||||||
</div>
|
{% if config.RECAPTCHA_ENABLE %}
|
||||||
|
{{ render_field(form.recaptcha) }}
|
||||||
<div class="form-group">
|
{% endif %}
|
||||||
<label for="email">Email</label>
|
{% endcall %}
|
||||||
<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>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% 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 %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
{% if session.get('user') %}
|
{% if g.current_user.is_authenticated() %}
|
||||||
<li class="dropdown user-avatar">
|
<li class="dropdown user-avatar">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||||
<span>
|
<span>
|
||||||
<img src="{{ session['user'].get('avatar') }}" class="menu-avatar">
|
<img src="{{ g.current_user.avatar }}" class="menu-avatar">
|
||||||
<span>{{ session['user'].get('username') }} <i class="icon-caret-down"></i></span>
|
<span>{{ g.current_user.username }} <i class="icon-caret-down"></i></span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<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>
|
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
@ -68,8 +68,6 @@
|
||||||
<!-- Page Menu -->
|
<!-- Page Menu -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="main-body">
|
<div id="main-body">
|
||||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
{% 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
|
bcrypt
|
||||||
Flask
|
Flask
|
||||||
Flask-Login
|
|
||||||
Flask-Assets
|
Flask-Assets
|
||||||
|
Flask-Cache
|
||||||
|
Flask-Login
|
||||||
Flask-Script
|
Flask-Script
|
||||||
|
Flask-SQLAlchemy
|
||||||
Flask-WTF
|
Flask-WTF
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
closure==20121212
|
closure==20121212
|
||||||
|
|
Loading…
Reference in a new issue