WIP commit.  Changed routes to POST/PUT/DELETE on page name endpoint to be more RESTful.
Check wiki dir permissions
Add comments
Add dummy favicon, robots.txt, humans.txt
Remove create.html (wasn't being used)
Fix version command
This commit is contained in:
Matthew Scragg 2014-10-20 17:27:38 -05:00
parent b99128e47a
commit e6bc4928c9
13 changed files with 321 additions and 144 deletions

View File

@ -1 +1 @@
0.3.19
0.3.20

View File

@ -210,7 +210,8 @@ assets.register('main.js',
'vendor/datatables/media/js/jquery.dataTables.js',
'vendor/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.js',
'js/hbs-helpers.js',
'js/mdr.js')
'js/mdr.js',
'js/main.js')
assets.register('main.css',
'vendor/bootswatch-dist/css/bootstrap.css',

View File

@ -330,8 +330,8 @@ def test():
def version():
""" Output version
"""
with open('VERSION') as f:
return f.read().strip()
with open(os.path.join(config.APP_PATH, 'VERSION')) as f:
click.echo(f.read().strip())
if __name__ == '__main__':

View File

@ -0,0 +1,13 @@
import os
import sys
from realms import app
from realms.modules.wiki.models import Wiki
# Init Wiki
Wiki(app.config['WIKI_PATH'])
# Check paths
for mode in [os.W_OK, os.R_OK]:
for dir_ in [app.config['WIKI_PATH'], os.path.join(app.config['WIKI_PATH'], '.git')]:
if not os.access(dir_, mode):
sys.exit('Read and write access to WIKI_PATH is required (%s)' % dir_)

View File

@ -14,15 +14,30 @@ from realms.lib.hook import HookMixin
def cname_to_filename(cname):
""" Convert canonical name to filename
:param cname: Canonical name
:return: str -- Filename
"""
return cname.lower() + ".md"
def filename_to_cname(filename):
""" It's assumed filename is already cname format
"""Convert filename to canonical name.
.. note::
It's assumed filename is already canonical format
"""
return os.path.splitext(filename)[0]
class PageNotFound(Exception):
pass
class Wiki(HookMixin):
path = None
base_path = '/'
@ -48,56 +63,42 @@ class Wiki(HookMixin):
return "Wiki: %s" % self.path
def revert_page(self, name, commit_sha, message, username):
"""Revert page to passed commit sha1
:param name: Name of page to revert.
:param commit_sha: Commit Sha1 to revert to.
:param message: Commit message.
:param username:
:return: Git commit sha1
"""
page = self.get_page(name, commit_sha)
if not page:
# Page not found
return None
commit_info = gittle.utils.git.commit_info(self.gittle[commit_sha.encode('latin-1')])
message = commit_info['message']
raise PageNotFound()
if not message:
commit_info = gittle.utils.git.commit_info(self.gittle[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):
"""Write page to git repo
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)) + "```"
:param name: Name of page.
:param content: Content of page.
:param message: Commit message.
:param create: Perform git add operation?
:param username: Commit Name.
:param email: Commit Email.
:return: Git commit sha1.
"""
cname = to_canonical(name)
# 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&gt;)", "\n>", content)
content = re.sub(r"(^&gt;)", ">", content)
# Handlebars partial ">"
content = re.sub(r"\{\{&gt;(.*?)\}\}", r'{{>\1}}', content)
# Handlebars, allow {{}} inside HTML links
content = content.replace("%7B", "{")
content = content.replace("%7D", "}")
content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL)
filename = cname_to_filename(cname)
content = self.clean(content)
with open(self.path + "/" + filename, 'w') as f:
f.write(content)
@ -122,7 +123,60 @@ class Wiki(HookMixin):
return ret
def clean(self, content):
"""Clean any HTML, this might not be necessary.
:param content: Content of page.
:return: str
"""
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={'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&gt;)", "\n>", content)
content = re.sub(r"(^&gt;)", ">", content)
# Handlebars partial ">"
content = re.sub(r"\{\{&gt;(.*?)\}\}", r'{{>\1}}', content)
# Handlebars, allow {{}} inside HTML links
content = content.replace("%7B", "{")
content = content.replace("%7D", "}")
content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL)
return content
def rename_page(self, old_name, new_name, user=None):
"""Rename page.
:param old_name: Page that will be renamed.
:param new_name: New name of page.
:param user: User object if any.
:return: str -- Commit sha1
"""
old_filename, new_filename = map(cname_to_filename, [old_name, new_name])
if old_filename not in self.gittle.index:
# old doesn't exist
@ -137,14 +191,38 @@ class Wiki(HookMixin):
self.gittle.add(new_filename)
self.gittle.rm(old_filename)
self.gittle.commit(name=getattr(user, 'username', self.default_committer_name),
email=getattr(user, 'email', self.default_committer_email),
message="Moved %s to %s" % (old_name, new_name),
files=[old_filename, new_filename])
commit = self.gittle.commit(name=getattr(user, 'username', self.default_committer_name),
email=getattr(user, 'email', self.default_committer_email),
message="Moved %s to %s" % (old_name, new_name),
files=[old_filename, new_filename])
cache.delete_many(old_filename, new_filename)
return commit
def delete_page(self, name, user=None):
"""Delete page.
:param name: Page that will be deleted
:param user: User object if any
:return: str -- Commit sha1
"""
self.gittle.rm(name)
commit = self.gittle.commit(name=getattr(user, 'username', self.default_committer_name),
email=getattr(user, 'email', self.default_committer_email),
message="Deleted %s" % name,
files=[name])
cache.delete_many(name)
return commit
def get_page(self, name, sha='HEAD'):
"""Get page data, partials, commit info.
:param name: Name of page.
:param sha: Commit sha.
:return: dict
"""
cached = cache.get(name)
if cached:
return cached
@ -172,22 +250,46 @@ class Wiki(HookMixin):
return None
def get_meta(self, content):
"""Get metadata from page if any.
:param content: Page content
:return: dict
"""
if not content.startswith("---"):
return None
meta_end = re.search("\n(\.{3}|\-{3})", content)
if not meta_end:
return None
try:
return yaml.safe_load(content[0:meta_end.start()])
except Exception as e:
return {'error': e.message}
def compare(self, name, old_sha, new_sha):
"""Compare two revisions of the same page.
:param name: Name of page.
:param old_sha: Older sha.
:param new_sha: Newer sha.
:return: str - Raw markup with styles
"""
# TODO: This could be effectively done in the browser
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_index(self):
"""Get repo index of head.
:return: list -- List of dicts
"""
rv = []
index = self.repo.open_index()
for name in index:
@ -201,8 +303,20 @@ class Wiki(HookMixin):
return rv
def get_history(self, name, limit=100):
"""Get page history.
:param name: Name of page.
:param limit: Limit history size.
:return: list -- List of dicts
"""
if not len(self.repo.open_index()):
# Index is empty, no commits
return []
file_path = cname_to_filename(name)
versions = []
walker = self.repo.get_walker(paths=[file_path], max_entries=limit)
for entry in walker:
change_type = None
@ -220,4 +334,3 @@ class Wiki(HookMixin):
type=change_type))
return versions

View File

@ -1,4 +1,4 @@
from flask import g, render_template, request, redirect, Blueprint, flash, url_for, current_app
from flask import abort, 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.modules.wiki.models import Wiki
@ -17,10 +17,11 @@ def commit(name, sha):
cname = to_canonical(name)
data = g.current_wiki.get_page(cname, sha=sha)
if data:
return render_template('wiki/page.html', name=name, page=data, commit=sha)
else:
return redirect(url_for('wiki.create', name=cname))
if not data:
abort(404)
return render_template('wiki/page.html', name=name, page=data, commit=sha)
@blueprint.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
@ -35,15 +36,17 @@ def revert():
name = request.form.get('name')
commit = request.form.get('commit')
cname = to_canonical(name)
message = request.form.get('message', "Reverting %s" % cname)
if cname in app.config.WIKI_LOCKED_PAGES:
flash("Page is locked")
return redirect(url_for(app.config['ROOT_ENDPOINT']))
if cname in app.config.get('WIKI_LOCKED_PAGES'):
return dict(error=True, message="Page is locked")
g.current_wiki.revert_page(name, commit, message="Reverting %s" % cname,
username=current_user.username)
flash('Page reverted', 'success')
return redirect(url_for('wiki.page', name=cname))
sha = g.current_wiki.revert_page(name, commit, message=message,
username=current_user.username)
if sha:
flash("Page reverted")
return dict(sha=sha)
@blueprint.route("/_history/<name>")
@ -51,74 +54,40 @@ def history(name):
return render_template('wiki/history.html', name=name, history=g.current_wiki.get_history(name))
@blueprint.route("/_edit/<name>", methods=['GET', 'POST'])
@blueprint.route("/_edit/<name>")
@login_required
def edit(name):
data = g.current_wiki.get_page(name)
cname = to_canonical(name)
if request.method == 'POST':
edit_cname = to_canonical(request.form['name'])
page = g.current_wiki.get_page(name)
if edit_cname in app.config['WIKI_LOCKED_PAGES']:
return redirect(url_for(app.config['ROOT_ENDPOINT']))
if not page:
# Page doesn't exist
return redirect(url_for('wiki.create', name=cname))
if edit_cname != cname.lower():
g.current_wiki.rename_page(cname, edit_cname)
g.current_wiki.write_page(edit_cname,
request.form['content'],
message=request.form['message'],
username=current_user.username)
else:
if data:
name = remove_ext(data['name'])
content = data.get('data')
g.assets['js'].append('editor.js')
return render_template('wiki/edit.html',
name=name,
content=content,
info=data.get('info'),
sha=data.get('sha'),
partials=data.get('partials'))
else:
return redirect(url_for('wiki.create', name=cname))
name = remove_ext(page['name'])
g.assets['js'].append('editor.js')
return render_template('wiki/edit.html',
name=name,
content=page.get('data'),
info=page.get('info'),
sha=page.get('sha'),
partials=page.get('partials'))
@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'])
@blueprint.route("/_create/", defaults={'name': None})
@blueprint.route("/_create/<name>")
@login_required
def create(name):
if request.method == 'POST':
cname = to_canonical(request.form['name'])
cname = to_canonical(name) if name else ""
if cname and g.current_wiki.get_page(cname):
# Page exists, edit instead
return redirect(url_for('wiki.edit', name=cname))
if cname in app.config['WIKI_LOCKED_PAGES']:
return redirect(url_for("wiki.create"))
if not cname:
return redirect(url_for("wiki.create"))
g.current_wiki.write_page(request.form['name'],
request.form['content'],
message=request.form['message'],
create=True,
username=current_user.username)
else:
cname = to_canonical(name) if name else ""
if cname and g.current_wiki.get_page(cname):
# Page exists, edit instead
return redirect(url_for('wiki.edit', name=cname))
g.assets['js'].append('editor.js')
return render_template('wiki/edit.html',
name=cname,
content="",
info={})
g.assets['js'].append('editor.js')
return render_template('wiki/edit.html',
name=cname,
content="",
info={})
@blueprint.route("/_index")
@ -126,6 +95,48 @@ def index():
return render_template('wiki/index.html', index=g.current_wiki.get_index())
@blueprint.route("/<name>", methods=['POST', 'PUT', 'DELETE'])
@login_required
def page_write(name):
cname = to_canonical(name)
if not cname:
return dict(error=True, message="Invalid name")
if request.method == 'POST':
# Create
if cname in app.config.get('WIKI_LOCKED_PAGES'):
return dict(error=True, message="Page is locked")
sha = g.current_wiki.write_page(cname,
request.form['content'],
message=request.form['message'],
create=True,
username=current_user.username)
elif request.method == 'PUT':
edit_cname = to_canonical(request.form['name'])
if edit_cname in app.config.get('WIKI_LOCKED_PAGES'):
return dict(error=True, message="Page is locked")
if edit_cname != cname.lower():
g.current_wiki.rename_page(cname, edit_cname)
sha = g.current_wiki.write_page(edit_cname,
request.form['content'],
message=request.form['message'],
username=current_user.username)
return dict(sha=sha)
else:
# DELETE
sha = g.current_wiki.delete_page(name, user=current_user)
return dict(sha=sha)
@blueprint.route("/", defaults={'name': 'home'})
@blueprint.route("/<name>")
def page(name):
@ -134,6 +145,7 @@ def page(name):
return redirect(url_for('wiki.page', name=cname))
data = g.current_wiki.get_page(cname)
if data:
return render_template('wiki/page.html', name=cname, page=data, partials=data.get('partials'))
else:

0
realms/static/humans.txt Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -2,6 +2,8 @@ var $entry_markdown_header = $("#entry-markdown-header");
var $entry_preview_header = $("#entry-preview-header");
var $entry_markdown = $(".entry-markdown");
var $entry_preview = $(".entry-preview");
var $page_name = $("#page-name");
var $page_message = $("#page-message");
// Tabs
$entry_markdown_header.click(function(){
@ -66,12 +68,26 @@ var aced = new Aced({
info: Commit.info,
submit: function(content) {
var data = {
name: $("#page-name").val(),
message: $("#page-message").val(),
name: $page_name.val(),
message: $page_message.val(),
content: content
};
$.post(window.location, data, function() {
location.href = Config['RELATIVE_PATH'] + '/' + data['name'];
var path = Config['RELATIVE_PATH'] + '/' + data['name'];
var type = (Commit.info['sha']) ? "PUT" : "POST";
$.ajax({
type: type,
url: path,
data: data,
dataType: 'json'
}).always(function(data, status, error) {
if (data && data['error']) {
$page_name.addClass('parsley-error');
bootbox.alert("<h3>" + data['message'] + "</h3>");
} else {
location.href = path;
}
});
}
});

40
realms/static/js/main.js Normal file
View File

@ -0,0 +1,40 @@
$(function(){
$(".ajax-form").submit(function(e){
e.preventDefault();
var submitting = 'submitting';
if ($(this).data(submitting)) {
return;
}
$(this).data(submitting, 1);
var action = $(this).attr('action');
var method = $(this).attr('method');
var redirect = $(this).data('redirect');
var data = $(this).serialize();
var req = $.ajax({
type: method,
url: action,
data: data,
dataType: 'json'
});
req.done(function() {
if (redirect) {
location.href = redirect;
}
});
req.fail(function(data, status, error) {
console.log(data);
});
req.always(function() {
$(this).removeData(submitting);
});
});
});

0
realms/static/robots.txt Normal file
View File

View File

@ -1,18 +0,0 @@
{% extends 'layout.html' %}
{% block body %}
<form role="form" method="post">
<div class="form-group">
<label for="name"></label>
<input id="name" type="text" class="form-control" name="name" placeholder="Page Name" value="{{- name -}}" />
</div>
<div class="form-group">
<label for="content"></label>
<textarea name="content" id="content" class="form-control" placeholder="Content"></textarea>
</div>
<input type="submit" class="btn btn-primary" value="Save" />
</form>
{% endblock %}

View File

@ -9,7 +9,7 @@
{% block body %}
{% if commit %}
<div id="page-action-bar">
<form method="POST" action="{{ url_for('wiki.revert') }}">
<form method="POST" action="{{ url_for('wiki.revert') }}" class="ajax-form" data-redirect="{{ url_for('wiki.page', name=name) }}">
<input type="hidden" value="{{ name }}" name="name" />
<input type="hidden" value="{{ commit }}" name="commit" />
<input type="submit" class="btn btn-danger btn-sm" title="Revert back to this revision" value="Revert" />