WIP
This commit is contained in:
parent
b02d3db684
commit
86f0549e44
24 changed files with 710 additions and 398 deletions
|
@ -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))
|
||||
else:
|
||||
flash('Email or Password Incorrect', 'warning')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
return render_template("auth/login.html")
|
||||
return render_template("auth/login.html", form=form)
|
||||
|
||||
@blueprint.route("/register")
|
||||
|
||||
@blueprint.route("/register", methods=['GET', 'POST'])
|
||||
def register():
|
||||
form = RegistrationForm()
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
if not form.validate():
|
||||
flash('Form invalid', 'warning')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
if User.get_by_username(request.form['username']):
|
||||
flash('Username is taken', 'warning')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
if User.get_by_email(request.form['email']):
|
||||
flash('Email is taken', 'warning')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
User.create(request.form['username'], request.form['email'], request.form['password'])
|
||||
User.auth(request.form['email'], request.form['password'])
|
||||
|
||||
return redirect(request.args.get("next") or url_for(config.ROOT_ENDPOINT))
|
||||
else:
|
||||
return render_template("auth/register.html")
|
||||
|
||||
return render_template("auth/register.html", form=form)
|
||||
|
||||
|
||||
@blueprint.route("/settings", methods=['GET', 'POST'])
|
||||
def settings():
|
||||
return render_template("auth/settings.html")
|
||||
|
||||
@blueprint.route("/logout")
|
||||
def logout():
|
||||
User.logout()
|
||||
return redirect("/")
|
|
@ -1,8 +1,7 @@
|
|||
from realms.lib.assets import register
|
||||
from realms import assets
|
||||
|
||||
register(
|
||||
'editor',
|
||||
'js/ace/ace.js',
|
||||
'js/ace/mode-markdown.js',
|
||||
'vendor/keymaster/keymaster.js',
|
||||
'js/dillinger.js')
|
||||
assets.register('editor',
|
||||
'js/ace/ace.js',
|
||||
'js/ace/mode-markdown.js',
|
||||
'vendor/keymaster/keymaster.js',
|
||||
'js/dillinger.js')
|
||||
|
|
165
realms/modules/wiki/models.py
Normal file
165
realms/modules/wiki/models.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
import os
|
||||
import re
|
||||
import lxml.html
|
||||
from lxml.html.clean import Cleaner
|
||||
import ghdiff
|
||||
import gittle.utils
|
||||
from gittle import Gittle
|
||||
from dulwich.repo import NotGitRepository
|
||||
from werkzeug.utils import escape, unescape
|
||||
from realms.lib.util import to_canonical
|
||||
from realms import cache
|
||||
|
||||
|
||||
class MyGittle(Gittle):
|
||||
|
||||
def file_history(self, path):
|
||||
"""Returns all commits where given file was modified
|
||||
"""
|
||||
versions = []
|
||||
commits_info = self.commit_info()
|
||||
seen_shas = set()
|
||||
|
||||
for commit in commits_info:
|
||||
try:
|
||||
files = self.get_commit_files(commit['sha'], paths=[path])
|
||||
file_path, file_data = files.items()[0]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
file_sha = file_data['sha']
|
||||
|
||||
if file_sha in seen_shas:
|
||||
continue
|
||||
else:
|
||||
seen_shas.add(file_sha)
|
||||
|
||||
versions.append(dict(author=commit['author']['name'],
|
||||
time=commit['time'],
|
||||
file_sha=file_sha,
|
||||
sha=commit['sha'],
|
||||
message=commit['message']))
|
||||
return versions
|
||||
|
||||
def mv_fs(self, file_pair):
|
||||
old_name, new_name = file_pair
|
||||
os.rename(self.path + "/" + old_name, self.path + "/" + new_name)
|
||||
|
||||
|
||||
class Wiki():
|
||||
path = None
|
||||
base_path = '/'
|
||||
default_ref = 'master'
|
||||
default_committer_name = 'Anon'
|
||||
default_committer_email = 'anon@anon.anon'
|
||||
index_page = 'home'
|
||||
repo = None
|
||||
|
||||
def __init__(self, path):
|
||||
try:
|
||||
self.repo = MyGittle(path)
|
||||
except NotGitRepository:
|
||||
self.repo = MyGittle.init(path)
|
||||
|
||||
self.path = path
|
||||
|
||||
def revert_page(self, name, commit_sha, message, username):
|
||||
page = self.get_page(name, commit_sha)
|
||||
if not page:
|
||||
# Page not found
|
||||
return None
|
||||
commit_info = gittle.utils.git.commit_info(self.repo[commit_sha.encode('latin-1')])
|
||||
message = commit_info['message']
|
||||
return self.write_page(name, page['data'], message=message, username=username)
|
||||
|
||||
def write_page(self, name, content, message=None, create=False, username=None, email=None):
|
||||
|
||||
def escape_repl(m):
|
||||
if m.group(1):
|
||||
return "```" + escape(m.group(1)) + "```"
|
||||
|
||||
def unescape_repl(m):
|
||||
if m.group(1):
|
||||
return "```" + unescape(m.group(1)) + "```"
|
||||
|
||||
# prevents p tag from being added, we remove this later
|
||||
content = '<div>' + content + '</div>'
|
||||
content = re.sub(r"```(.*?)```", escape_repl, content, flags=re.DOTALL)
|
||||
|
||||
tree = lxml.html.fromstring(content)
|
||||
|
||||
cleaner = Cleaner(remove_unknown_tags=False,
|
||||
kill_tags=set(['style']),
|
||||
safe_attrs_only=False)
|
||||
tree = cleaner.clean_html(tree)
|
||||
|
||||
content = lxml.html.tostring(tree, encoding='utf-8', method='html')
|
||||
|
||||
# remove added div tags
|
||||
content = content[5:-6]
|
||||
|
||||
# FIXME this is for block quotes, doesn't work for double ">"
|
||||
content = re.sub(r"(\n>)", "\n>", content)
|
||||
content = re.sub(r"(^>)", ">", content)
|
||||
|
||||
content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL)
|
||||
|
||||
cname = to_canonical(name)
|
||||
filename = self.cname_to_filename(cname)
|
||||
with open(self.path + "/" + filename, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
if create:
|
||||
self.repo.add(filename)
|
||||
|
||||
if not message:
|
||||
message = "Updated %s" % name
|
||||
|
||||
if not username:
|
||||
username = self.default_committer_name
|
||||
|
||||
if not email:
|
||||
email = self.default_committer_email
|
||||
|
||||
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)])
|
||||
self.repo.commit(name=self.default_committer_name,
|
||||
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')
|
||||
sha = sha.encode('latin-1')
|
||||
|
||||
try:
|
||||
return self.repo.get_commit_files(sha, paths=[name]).get(name)
|
||||
except KeyError:
|
||||
# HEAD doesn't exist yet
|
||||
return None
|
||||
|
||||
def compare(self, name, old_sha, new_sha):
|
||||
old = self.get_page(name, sha=old_sha)
|
||||
new = self.get_page(name, sha=new_sha)
|
||||
return ghdiff.diff(old['data'], new['data'])
|
||||
|
||||
def get_history(self, name):
|
||||
return self.repo.file_history(self.cname_to_filename(name))
|
||||
|
||||
@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'))
|
||||
flash('Page reverted', 'success')
|
||||
return redirect(url_for('wiki.page', name=cname))
|
||||
name = request.form.get('name')
|
||||
commit = request.form.get('commit')
|
||||
cname = to_canonical(name)
|
||||
wiki.revert_page(name, commit, message="Reverting %s" % cname,
|
||||
username=g.current_user.username)
|
||||
flash('Page reverted', 'success')
|
||||
return redirect(url_for('wiki.page', name=cname))
|
||||
|
||||
|
||||
@blueprint.route("/_history/<name>")
|
||||
def history(name):
|
||||
history = g.current_wiki.get_history(name)
|
||||
history = wiki.get_history(name)
|
||||
return render_template('wiki/history.html', name=name, history=history, wiki_home=url_for('wiki.page'))
|
||||
|
||||
|
||||
@blueprint.route("/_edit/<name>", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit(name):
|
||||
data = g.current_wiki.get_page(name)
|
||||
data = wiki.get_page(name)
|
||||
cname = to_canonical(name)
|
||||
if request.method == 'POST':
|
||||
edit_cname = to_canonical(request.form['name'])
|
||||
if edit_cname.lower() != cname.lower():
|
||||
g.current_wiki.rename_page(cname, edit_cname)
|
||||
wiki.rename_page(cname, edit_cname)
|
||||
|
||||
g.current_wiki.write_page(edit_cname,
|
||||
request.form['content'],
|
||||
message=request.form['message'],
|
||||
username=g.current_user.get('username'))
|
||||
wiki.write_page(edit_cname,
|
||||
request.form['content'],
|
||||
message=request.form['message'],
|
||||
username=g.current_user.username)
|
||||
else:
|
||||
if data:
|
||||
name = remove_ext(data['name'])
|
||||
|
@ -64,22 +69,24 @@ def edit(name):
|
|||
|
||||
|
||||
@blueprint.route("/_delete/<name>", methods=['POST'])
|
||||
@login_required
|
||||
def delete(name):
|
||||
pass
|
||||
|
||||
|
||||
@blueprint.route("/_create/", defaults={'name': None}, methods=['GET', 'POST'])
|
||||
@blueprint.route("/_create/<name>", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create(name):
|
||||
if request.method == 'POST':
|
||||
g.current_wiki.write_page(request.form['name'],
|
||||
request.form['content'],
|
||||
message=request.form['message'],
|
||||
create=True,
|
||||
username=g.current_user.get('username'))
|
||||
wiki.write_page(request.form['name'],
|
||||
request.form['content'],
|
||||
message=request.form['message'],
|
||||
create=True,
|
||||
username=g.current_user.username)
|
||||
else:
|
||||
cname = to_canonical(name) if name else ""
|
||||
if cname and g.current_wiki.get_page(cname):
|
||||
if cname and wiki.get_page(cname):
|
||||
# Page exists, edit instead
|
||||
return redirect(url_for('wiki.edit', name=cname))
|
||||
|
||||
|
@ -94,7 +101,7 @@ def page(name):
|
|||
if cname != name:
|
||||
return redirect(url_for('wiki.page', name=cname))
|
||||
|
||||
data = g.current_wiki.get_page(cname)
|
||||
data = wiki.get_page(cname)
|
||||
|
||||
if data:
|
||||
return render_template('wiki/page.html', name=cname, page=data)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue