diff --git a/VERSION b/VERSION
index 719cd12..771f209 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.3.19
\ No newline at end of file
+0.3.20
\ No newline at end of file
diff --git a/realms/__init__.py b/realms/__init__.py
index 6875fae..a5c0481 100644
--- a/realms/__init__.py
+++ b/realms/__init__.py
@@ -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',
diff --git a/realms/cli.py b/realms/cli.py
index a8a7e89..be4237a 100644
--- a/realms/cli.py
+++ b/realms/cli.py
@@ -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__':
diff --git a/realms/modules/wiki/__init__.py b/realms/modules/wiki/__init__.py
index e69de29..2c61d69 100644
--- a/realms/modules/wiki/__init__.py
+++ b/realms/modules/wiki/__init__.py
@@ -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_)
diff --git a/realms/modules/wiki/models.py b/realms/modules/wiki/models.py
index 6f87a20..5d6b3fa 100644
--- a/realms/modules/wiki/models.py
+++ b/realms/modules/wiki/models.py
@@ -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 = '
' + content + '
'
- 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 = '' + content + '
'
+ 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),
- 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
-
diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py
index 503dd7b..84e0078 100644
--- a/realms/modules/wiki/views.py
+++ b/realms/modules/wiki/views.py
@@ -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//")
@@ -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/")
@@ -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/", methods=['GET', 'POST'])
+@blueprint.route("/_edit/")
@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/", methods=['POST'])
-@login_required
-def delete(name):
- pass
-
-
-@blueprint.route("/_create/", defaults={'name': None}, methods=['GET', 'POST'])
-@blueprint.route("/_create/", methods=['GET', 'POST'])
+@blueprint.route("/_create/", defaults={'name': None})
+@blueprint.route("/_create/")
@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("/", 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("/")
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:
diff --git a/realms/static/humans.txt b/realms/static/humans.txt
new file mode 100644
index 0000000..e69de29
diff --git a/realms/static/img/favicon.ico b/realms/static/img/favicon.ico
new file mode 100644
index 0000000..ba48efe
Binary files /dev/null and b/realms/static/img/favicon.ico differ
diff --git a/realms/static/js/editor.js b/realms/static/js/editor.js
index 1c3635d..249fc0d 100644
--- a/realms/static/js/editor.js
+++ b/realms/static/js/editor.js
@@ -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("" + data['message'] + "
");
+ } else {
+ location.href = path;
+ }
});
}
});
\ No newline at end of file
diff --git a/realms/static/js/main.js b/realms/static/js/main.js
new file mode 100644
index 0000000..5c7e375
--- /dev/null
+++ b/realms/static/js/main.js
@@ -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);
+ });
+
+ });
+});
\ No newline at end of file
diff --git a/realms/static/robots.txt b/realms/static/robots.txt
new file mode 100644
index 0000000..e69de29
diff --git a/realms/templates/wiki/create.html b/realms/templates/wiki/create.html
deleted file mode 100644
index 6a7775c..0000000
--- a/realms/templates/wiki/create.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends 'layout.html' %}
-{% block body %}
-
-
-
-{% endblock %}
\ No newline at end of file
diff --git a/realms/templates/wiki/page.html b/realms/templates/wiki/page.html
index ce97782..bd40d7f 100644
--- a/realms/templates/wiki/page.html
+++ b/realms/templates/wiki/page.html
@@ -9,7 +9,7 @@
{% block body %}
{% if commit %}
-