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/media/js/jquery.dataTables.js',
'vendor/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.js', 'vendor/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.js',
'js/hbs-helpers.js', 'js/hbs-helpers.js',
'js/mdr.js') 'js/mdr.js',
'js/main.js')
assets.register('main.css', assets.register('main.css',
'vendor/bootswatch-dist/css/bootstrap.css', 'vendor/bootswatch-dist/css/bootstrap.css',

View file

@ -330,8 +330,8 @@ def test():
def version(): def version():
""" Output version """ Output version
""" """
with open('VERSION') as f: with open(os.path.join(config.APP_PATH, 'VERSION')) as f:
return f.read().strip() click.echo(f.read().strip())
if __name__ == '__main__': 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): def cname_to_filename(cname):
""" Convert canonical name to filename
:param cname: Canonical name
:return: str -- Filename
"""
return cname.lower() + ".md" return cname.lower() + ".md"
def filename_to_cname(filename): 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] return os.path.splitext(filename)[0]
class PageNotFound(Exception):
pass
class Wiki(HookMixin): class Wiki(HookMixin):
path = None path = None
base_path = '/' base_path = '/'
@ -48,56 +63,42 @@ class Wiki(HookMixin):
return "Wiki: %s" % self.path return "Wiki: %s" % self.path
def revert_page(self, name, commit_sha, message, username): 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) page = self.get_page(name, commit_sha)
if not page: if not page:
# Page not found raise PageNotFound()
return None
if not message:
commit_info = gittle.utils.git.commit_info(self.gittle[commit_sha.encode('latin-1')]) commit_info = gittle.utils.git.commit_info(self.gittle[commit_sha.encode('latin-1')])
message = commit_info['message'] message = commit_info['message']
return self.write_page(name, page['data'], message=message, username=username) 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 write_page(self, name, content, message=None, create=False, username=None, email=None):
"""Write page to git repo
def escape_repl(m): :param name: Name of page.
if m.group(1): :param content: Content of page.
return "```" + escape(m.group(1)) + "```" :param message: Commit message.
:param create: Perform git add operation?
def unescape_repl(m): :param username: Commit Name.
if m.group(1): :param email: Commit Email.
return "```" + unescape(m.group(1)) + "```" :return: Git commit sha1.
"""
cname = to_canonical(name) 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) filename = cname_to_filename(cname)
content = self.clean(content)
with open(self.path + "/" + filename, 'w') as f: with open(self.path + "/" + filename, 'w') as f:
f.write(content) f.write(content)
@ -122,7 +123,60 @@ class Wiki(HookMixin):
return ret 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): 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]) old_filename, new_filename = map(cname_to_filename, [old_name, new_name])
if old_filename not in self.gittle.index: if old_filename not in self.gittle.index:
# old doesn't exist # old doesn't exist
@ -137,14 +191,38 @@ class Wiki(HookMixin):
self.gittle.add(new_filename) self.gittle.add(new_filename)
self.gittle.rm(old_filename) self.gittle.rm(old_filename)
self.gittle.commit(name=getattr(user, 'username', self.default_committer_name), commit = self.gittle.commit(name=getattr(user, 'username', self.default_committer_name),
email=getattr(user, 'email', self.default_committer_email), email=getattr(user, 'email', self.default_committer_email),
message="Moved %s to %s" % (old_name, new_name), message="Moved %s to %s" % (old_name, new_name),
files=[old_filename, new_filename]) files=[old_filename, new_filename])
cache.delete_many(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'): 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) cached = cache.get(name)
if cached: if cached:
return cached return cached
@ -172,22 +250,46 @@ class Wiki(HookMixin):
return None return None
def get_meta(self, content): def get_meta(self, content):
"""Get metadata from page if any.
:param content: Page content
:return: dict
"""
if not content.startswith("---"): if not content.startswith("---"):
return None return None
meta_end = re.search("\n(\.{3}|\-{3})", content) meta_end = re.search("\n(\.{3}|\-{3})", content)
if not meta_end: if not meta_end:
return None return None
try: try:
return yaml.safe_load(content[0:meta_end.start()]) return yaml.safe_load(content[0:meta_end.start()])
except Exception as e: except Exception as e:
return {'error': e.message} return {'error': e.message}
def compare(self, name, old_sha, new_sha): 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) old = self.get_page(name, sha=old_sha)
new = self.get_page(name, sha=new_sha) new = self.get_page(name, sha=new_sha)
return ghdiff.diff(old['data'], new['data']) return ghdiff.diff(old['data'], new['data'])
def get_index(self): def get_index(self):
"""Get repo index of head.
:return: list -- List of dicts
"""
rv = [] rv = []
index = self.repo.open_index() index = self.repo.open_index()
for name in index: for name in index:
@ -201,8 +303,20 @@ class Wiki(HookMixin):
return rv return rv
def get_history(self, name, limit=100): 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) file_path = cname_to_filename(name)
versions = [] versions = []
walker = self.repo.get_walker(paths=[file_path], max_entries=limit) walker = self.repo.get_walker(paths=[file_path], max_entries=limit)
for entry in walker: for entry in walker:
change_type = None change_type = None
@ -220,4 +334,3 @@ class Wiki(HookMixin):
type=change_type)) type=change_type))
return versions 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 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.modules.wiki.models import Wiki from realms.modules.wiki.models import Wiki
@ -17,10 +17,11 @@ def commit(name, sha):
cname = to_canonical(name) cname = to_canonical(name)
data = g.current_wiki.get_page(cname, sha=sha) data = g.current_wiki.get_page(cname, sha=sha)
if data:
if not data:
abort(404)
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:
return redirect(url_for('wiki.create', name=cname))
@blueprint.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>") @blueprint.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
@ -35,15 +36,17 @@ def revert():
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)
message = request.form.get('message', "Reverting %s" % cname)
if cname in app.config.WIKI_LOCKED_PAGES: if cname in app.config.get('WIKI_LOCKED_PAGES'):
flash("Page is locked") return dict(error=True, message="Page is locked")
return redirect(url_for(app.config['ROOT_ENDPOINT']))
g.current_wiki.revert_page(name, commit, message="Reverting %s" % cname, sha = g.current_wiki.revert_page(name, commit, message=message,
username=current_user.username) username=current_user.username)
flash('Page reverted', 'success') if sha:
return redirect(url_for('wiki.page', name=cname)) flash("Page reverted")
return dict(sha=sha)
@blueprint.route("/_history/<name>") @blueprint.route("/_history/<name>")
@ -51,64 +54,30 @@ def history(name):
return render_template('wiki/history.html', name=name, history=g.current_wiki.get_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 @login_required
def edit(name): def edit(name):
data = g.current_wiki.get_page(name)
cname = to_canonical(name) cname = to_canonical(name)
if request.method == 'POST': page = g.current_wiki.get_page(name)
edit_cname = to_canonical(request.form['name'])
if edit_cname in app.config['WIKI_LOCKED_PAGES']: if not page:
return redirect(url_for(app.config['ROOT_ENDPOINT'])) # Page doesn't exist
return redirect(url_for('wiki.create', name=cname))
if edit_cname != cname.lower(): name = remove_ext(page['name'])
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') g.assets['js'].append('editor.js')
return render_template('wiki/edit.html', return render_template('wiki/edit.html',
name=name, name=name,
content=content, content=page.get('data'),
info=data.get('info'), info=page.get('info'),
sha=data.get('sha'), sha=page.get('sha'),
partials=data.get('partials')) partials=page.get('partials'))
else:
return redirect(url_for('wiki.create', name=cname))
@blueprint.route("/_delete/<name>", methods=['POST']) @blueprint.route("/_create/", defaults={'name': None})
@login_required @blueprint.route("/_create/<name>")
def delete(name):
pass
@blueprint.route("/_create/", defaults={'name': None}, methods=['GET', 'POST'])
@blueprint.route("/_create/<name>", methods=['GET', 'POST'])
@login_required @login_required
def create(name): def create(name):
if request.method == 'POST':
cname = to_canonical(request.form['name'])
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 "" cname = to_canonical(name) if name else ""
if cname and g.current_wiki.get_page(cname): if cname and g.current_wiki.get_page(cname):
# Page exists, edit instead # Page exists, edit instead
@ -126,6 +95,48 @@ def index():
return render_template('wiki/index.html', index=g.current_wiki.get_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("/", defaults={'name': 'home'})
@blueprint.route("/<name>") @blueprint.route("/<name>")
def page(name): def page(name):
@ -134,6 +145,7 @@ def page(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 = g.current_wiki.get_page(cname)
if data: if data:
return render_template('wiki/page.html', name=cname, page=data, partials=data.get('partials')) return render_template('wiki/page.html', name=cname, page=data, partials=data.get('partials'))
else: 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_preview_header = $("#entry-preview-header");
var $entry_markdown = $(".entry-markdown"); var $entry_markdown = $(".entry-markdown");
var $entry_preview = $(".entry-preview"); var $entry_preview = $(".entry-preview");
var $page_name = $("#page-name");
var $page_message = $("#page-message");
// Tabs // Tabs
$entry_markdown_header.click(function(){ $entry_markdown_header.click(function(){
@ -66,12 +68,26 @@ var aced = new Aced({
info: Commit.info, info: Commit.info,
submit: function(content) { submit: function(content) {
var data = { var data = {
name: $("#page-name").val(), name: $page_name.val(),
message: $("#page-message").val(), message: $page_message.val(),
content: content 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 %} {% block body %}
{% if commit %} {% if commit %}
<div id="page-action-bar"> <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="{{ name }}" name="name" />
<input type="hidden" value="{{ commit }}" name="commit" /> <input type="hidden" value="{{ commit }}" name="commit" />
<input type="submit" class="btn btn-danger btn-sm" title="Revert back to this revision" value="Revert" /> <input type="submit" class="btn btn-danger btn-sm" title="Revert back to this revision" value="Revert" />