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:
parent
b99128e47a
commit
e6bc4928c9
|
@ -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',
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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_)
|
|
@ -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
|
||||
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>)", "\n>", content)
|
||||
content = re.sub(r"(^>)", ">", content)
|
||||
|
||||
# Handlebars partial ">"
|
||||
content = re.sub(r"\{\{>(.*?)\}\}", 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>)", "\n>", content)
|
||||
content = re.sub(r"(^>)", ">", content)
|
||||
|
||||
# Handlebars partial ">"
|
||||
content = re.sub(r"\{\{>(.*?)\}\}", 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),
|
||||
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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
if not data:
|
||||
abort(404)
|
||||
|
||||
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>")
|
||||
|
@ -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,
|
||||
sha = g.current_wiki.revert_page(name, commit, message=message,
|
||||
username=current_user.username)
|
||||
flash('Page reverted', 'success')
|
||||
return redirect(url_for('wiki.page', name=cname))
|
||||
if sha:
|
||||
flash("Page reverted")
|
||||
|
||||
return dict(sha=sha)
|
||||
|
||||
|
||||
@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))
|
||||
|
||||
|
||||
@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')
|
||||
name = remove_ext(page['name'])
|
||||
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))
|
||||
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'])
|
||||
|
||||
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
|
||||
|
@ -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
0
realms/static/humans.txt
Normal file
BIN
realms/static/img/favicon.ico
Normal file
BIN
realms/static/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
|
@ -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
40
realms/static/js/main.js
Normal 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
0
realms/static/robots.txt
Normal 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 %}
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in a new issue