Merge branch 'master' into oauth_redirect

# Conflicts:
#	realms/modules/auth/views.py
This commit is contained in:
Chase Sterling 2016-09-04 12:25:57 -04:00
commit 2ce6c2d314
41 changed files with 350 additions and 213 deletions

View file

@ -1,6 +1,6 @@
from realms import login_manager
from flask import request, flash, redirect
from flask.ext.login import login_url
from flask_login import login_url
modules = set()

View file

@ -1,5 +1,5 @@
from flask import render_template
from flask.ext.login import login_user
from flask_login import login_user
from realms import ldap
from flask_ldap_login import LDAPLoginForm
from ..models import BaseUser

View file

@ -2,10 +2,10 @@ import click
from realms.lib.util import random_string
from realms.modules.auth.local.models import User
from realms.lib.util import green, red, yellow
from realms import flask_cli
from realms import cli_group
@flask_cli.group(short_help="Auth Module")
@cli_group(short_help="Auth Module")
def cli():
pass

View file

@ -1,5 +1,5 @@
from flask import current_app, render_template
from flask.ext.login import logout_user, login_user
from flask_login import logout_user, login_user
from realms import login_manager, db
from realms.lib.model import Model
from ..models import BaseUser

View file

@ -1,5 +1,5 @@
from flask import current_app
from flask.ext.login import UserMixin, logout_user, AnonymousUserMixin
from flask_login import UserMixin, logout_user, AnonymousUserMixin
from realms import login_manager
from realms.lib.util import gravatar_url
from itsdangerous import URLSafeSerializer, BadSignature

View file

@ -40,7 +40,7 @@ providers = {
'field_map': {
'id': 'id',
'username': 'login',
'email': 'email'
'email': lambda(data): data.get('email') or data['login'] + '@users.noreply.github.com'
},
'token_name': 'access_token'
},
@ -118,6 +118,8 @@ class User(BaseUser):
def get_value(d, key):
if isinstance(key, basestring):
return d.get(key)
elif callable(key):
return key(d)
# key should be list here
val = d.get(key.pop(0))
if len(key) == 0:

View file

@ -0,0 +1,29 @@
{% from 'macros.html' import render_form, render_field %}
{% if config.get('AUTH_LOCAL_ENABLE') %}
<button type="button" class="btn btn-info" data-toggle="modal" data-target="#ldap-modal">
<i class="fa fa-folder-open-o"></i>&nbsp; Login with LDAP
</button>
<div class="modal fade" id="ldap-modal" tabindex="-1" role="dialog" aria-labelledby="ldap-login">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="ldap-login">LDAP Login</h4>
</div>
<div class="modal-body">
{% call render_form(form, action_url=url_for('auth.ldap.login'), action_text='Login', btn_class='btn btn-primary') %}
{{ render_field(form.username, placeholder='Username', type='text', required=1) }}
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
{% endcall %}
</div>
</div>
</div>
</div>
{% else %}
<h3><i class="fa fa-folder-open-o"></i>&nbsp;LDAP Login</h3>
{% call render_form(form, action_url=url_for('auth.ldap.login'), action_text='Login', btn_class='btn btn-primary') %}
{{ render_field(form.username, placeholder='Username', type='text', required=1) }}
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
{% endcall %}
{% endif %}

View file

@ -0,0 +1,5 @@
{% from 'macros.html' import render_form, render_field %}
{% call render_form(form, action_url=url_for('auth.local.login'), action_text='Login', btn_class='btn btn-primary') %}
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
{% endcall %}

View file

@ -0,0 +1,4 @@
{% extends 'layout.html' %}
{% block body %}
{{ forms|safe }}
{% endblock %}

View file

@ -0,0 +1,13 @@
{% extends 'layout.html' %}
{% from 'macros.html' import render_form, render_field %}
{% block body %}
{% call render_form(form, action_url=url_for('auth.local.register'), action_text='Register', btn_class='btn btn-primary') %}
{{ render_field(form.username, placeholder='Username', type='username', **{"required": 1, "data-parsley-type": "alphanum"}) }}
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
{{ render_field(form.password, placeholder='Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }}
{{ render_field(form.confirm, placeholder='Confirm Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }}
{% if config.RECAPTCHA_ENABLE %}
{{ render_field(form.recaptcha) }}
{% endif %}
{% endcall %}
{% endblock %}

View file

@ -0,0 +1,5 @@
{% extends 'layout.html' %}
{% from 'macros.html' import render_form, render_field %}
{% block body %}
{% endblock %}

View file

@ -1,8 +1,8 @@
from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for, session
from flask.ext.login import logout_user
from flask_login import logout_user
from realms.modules.auth.models import Auth
blueprint = Blueprint('auth', __name__)
blueprint = Blueprint('auth', __name__, template_folder='templates')
@blueprint.route("/login", methods=['GET', 'POST'])

View file

@ -1,10 +1,10 @@
import click
from realms import create_app, search, flask_cli
from flask import current_app
from realms import search, cli_group
from realms.modules.wiki.models import Wiki
from realms.lib.util import filename_to_cname
@flask_cli.group(short_help="Search Module")
@cli_group(short_help="Search Module")
def cli():
pass
@ -13,27 +13,25 @@ def cli():
def rebuild_index():
""" Rebuild search index
"""
app = create_app()
if app.config.get('SEARCH_TYPE') == 'simple':
if current_app.config.get('SEARCH_TYPE') == 'simple':
click.echo("Search type is simple, try using elasticsearch.")
return
with app.app_context():
# Wiki
search.delete_index('wiki')
wiki = Wiki(app.config['WIKI_PATH'])
for entry in wiki.get_index():
page = wiki.get_page(entry['name'])
if not page:
# Some non-markdown files may have issues
continue
name = filename_to_cname(page['path'])
# TODO add email?
body = dict(name=name,
content=page.data,
message=page.info['message'],
username=page.info['author'],
updated_on=entry['mtime'],
created_on=entry['ctime'])
search.index_wiki(name, body)
# Wiki
search.delete_index('wiki')
wiki = Wiki(current_app.config['WIKI_PATH'])
for entry in wiki.get_index():
page = wiki.get_page(entry['name'])
if not page:
# Some non-markdown files may have issues
continue
# TODO add email?
# TODO I have concens about indexing the commit info from latest revision, see #148
info = next(page.history)
body = dict(name=page.name,
content=page.data,
message=info['message'],
username=info['author'],
updated_on=entry['mtime'],
created_on=entry['ctime'])
search.index_wiki(page.name, body)

View file

@ -14,7 +14,7 @@ def whoosh(app):
def elasticsearch(app):
from flask.ext.elastic import Elastic
from flask_elastic import Elastic
fields = app.config.get('ELASTICSEARCH_FIELDS')
return ElasticSearch(Elastic(app), fields)

View file

@ -0,0 +1,18 @@
{% extends 'layout.html' %}
{% block body %}
{% if results %}
<h3>Results for <em class="text-info">{{ request.args.get('q') }}</em></h3>
<div class="list-group">
{% for r in results %}
<a href="{{ url_for('wiki.page', name=r['name']) }}" class="list-group-item">
<h4 class="list-group-item-heading">{{ r['name'] }}</h4>
<p class="list-group-item-text">
{{ r['content'][:100] }}
</p>
</a>
{% endfor %}
</div>
{% else %}
<h3>No results found for <em class="text-info">{{ request.args.get('q') }}</em></h3>
{% endif %}
{% endblock %}

View file

@ -1,10 +1,14 @@
from flask import render_template, request, Blueprint
from flask import render_template, request, Blueprint, current_app
from flask.ext.login import current_user
from realms import search as search_engine
blueprint = Blueprint('search', __name__)
blueprint = Blueprint('search', __name__, template_folder='templates')
@blueprint.route('/_search')
def search():
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
return current_app.login_manager.unauthorized()
results = search_engine.wiki(request.args.get('q'))
return render_template('search/search.html', results=results)

View file

@ -7,4 +7,4 @@ assets.register('editor.js',
'vendor/ace-builds/src/mode-markdown.js',
'vendor/ace-builds/src/ext-keybinding_menu.js',
'vendor/keymaster/keymaster.js',
'js/aced.js')
'wiki/js/aced.js')

View file

@ -2,11 +2,9 @@ import os
import posixpath
import re
import ghdiff
import gittle.utils
import yaml
from gittle import Gittle
from dulwich.object_store import tree_lookup_path
from dulwich.repo import NotGitRepository
from dulwich.repo import Repo, NotGitRepository
from realms.lib.util import cname_to_filename, filename_to_cname
from realms import cache
from realms.lib.hook import HookMixin
@ -23,17 +21,13 @@ class Wiki(HookMixin):
default_committer_name = 'Anon'
default_committer_email = 'anon@anon.anon'
index_page = 'home'
gittle = None
repo = None
def __init__(self, path):
try:
self.gittle = Gittle(path)
self.repo = Repo(path)
except NotGitRepository:
self.gittle = Gittle.init(path)
# Dulwich repo
self.repo = self.gittle.repo
self.repo = Repo.init(path, mkdir=True)
self.path = path
@ -46,20 +40,20 @@ class Wiki(HookMixin):
:param name: Committer name
:param email: Committer email
:param message: Commit message
:param files: list of file names that should be committed
:param files: list of file names that will be staged for commit
:return:
"""
# Dulwich and gittle seem to want us to encode ourselves at the moment. see #152
if isinstance(name, unicode):
name = name.encode('utf-8')
if isinstance(email, unicode):
email = email.encode('utf-8')
if isinstance(message, unicode):
message = message.encode('utf-8')
return self.gittle.commit(name=name,
email=email,
message=message,
files=files)
author = committer = "%s <%s>" % (name, email)
self.repo.stage(files)
return self.repo.do_commit(message=message,
committer=committer,
author=author)
def get_page(self, name, sha='HEAD'):
"""Get page data, partials, commit info.
@ -104,52 +98,93 @@ class WikiPage(HookMixin):
if cached:
return cached
data = self.wiki.gittle.get_commit_files(self.sha, paths=[self.filename]).get(self.filename).get('data')
mode, sha = tree_lookup_path(self.wiki.repo.get_object, self.wiki.repo[self.sha].tree, self.filename)
data = self.wiki.repo[sha].data
cache.set(cache_key, data)
return data
@property
def info(self):
cache_key = self._cache_key('info')
cached = cache.get(cache_key)
if cached:
return cached
info = self.get_history(limit=1)[0]
cache.set(cache_key, info)
return info
def get_history(self, limit=100):
def history(self):
"""Get page history.
:param limit: Limit history size.
:return: list -- List of dicts
History can take a long time to generate for repositories with many commits.
This returns an iterator to avoid having to load them all at once, and caches
as it goes.
:return: iter -- Iterator over dicts
"""
cache_head = []
cache_tail = cache.get(self._cache_key('history')) or [{'_cache_missing': True}]
while True:
if not cache_tail:
return
for index, cached_rev in enumerate(cache_tail):
if cached_rev.get("_cache_missing"):
break
else:
yield cached_rev
cache_head.extend(cache_tail[:index])
cache_tail = cache_tail[index+1:]
start_sha = cached_rev.get('sha')
end_sha = cache_tail[0].get('sha') if cache_tail else None
for rev in self._iter_revs(start_sha=start_sha, end_sha=end_sha, filename=cached_rev.get('filename')):
cache_head.append(rev)
placeholder = {
'_cache_missing': True,
'sha': rev['sha'],
'filename': rev['new_filename']
}
cache.set(self._cache_key('history'), cache_head + [placeholder] + cache_tail)
yield rev
cache.set(self._cache_key('history'), cache_head + cache_tail)
def _iter_revs(self, start_sha=None, end_sha=None, filename=None):
if end_sha:
end_sha = [end_sha]
if not len(self.wiki.repo.open_index()):
# Index is empty, no commits
return []
versions = []
walker = self.wiki.repo.get_walker(paths=[self.filename], max_entries=limit)
return
filename = filename or self.filename
walker = iter(self.wiki.repo.get_walker(paths=[filename],
include=start_sha,
exclude=end_sha,
follow=True))
if start_sha:
# If we are not starting from HEAD, we already have the start commit
next(walker)
filename = self.filename
for entry in walker:
change_type = None
for change in entry.changes():
if change.old.path == self.filename:
if change.new.path == filename:
filename = change.old.path
change_type = change.type
elif change.new.path == self.filename:
change_type = change.type
author_name, author_email = entry.commit.author.rstrip('>').split('<')
versions.append(dict(
author=author_name.strip(),
author_email=author_email,
time=entry.commit.author_time,
message=entry.commit.message,
sha=entry.commit.id,
type=change_type))
break
return versions
author_name, author_email = entry.commit.author.rstrip('>').split('<')
r = dict(author=author_name.strip(),
author_email=author_email,
time=entry.commit.author_time,
message=entry.commit.message,
sha=entry.commit.id,
type=change_type,
new_filename=change.new.path,
old_filename=change.old.path)
yield r
@property
def history_cache(self):
"""Get info about the history cache.
:return: tuple -- (cached items, cache complete?)
"""
cached_revs = cache.get(self._cache_key('history'))
if not cached_revs:
return 0, False
elif any(rev.get('_cache_missing') for rev in cached_revs):
return len(cached_revs) - 1, False
return len(cached_revs), True
@property
def partials(self):
@ -196,8 +231,14 @@ class WikiPage(HookMixin):
return username, email
def _clear_cache(self):
cache.delete_many(*(self._cache_key(p) for p in ['data', 'info']))
def _invalidate_cache(self, save_history=None):
cache.delete(self._cache_key('data'))
if save_history:
if not save_history[0].get('_cache_missing'):
save_history = [{'_cache_missing': True}] + save_history
cache.set(self._cache_key('history'), save_history)
else:
cache.delete(self._cache_key('history'))
def delete(self, username=None, email=None, message=None):
"""Delete page.
@ -211,14 +252,12 @@ class WikiPage(HookMixin):
if not message:
message = "Deleted %s" % self.name
# gittle.rm won't actually remove the file, have to do it ourselves
os.remove(os.path.join(self.wiki.path, self.filename))
self.wiki.gittle.rm(self.filename)
commit = self.wiki.commit(name=username,
email=email,
message=message,
files=[self.filename])
self._clear_cache()
self._invalidate_cache()
return commit
def rename(self, new_name, username=None, email=None, message=None):
@ -232,7 +271,7 @@ class WikiPage(HookMixin):
"""
assert self.sha == 'HEAD'
old_filename, new_filename = self.filename, cname_to_filename(new_name)
if old_filename not in self.wiki.gittle.index:
if old_filename not in self.wiki.repo.open_index():
# old doesn't exist
return None
elif old_filename == new_filename:
@ -247,29 +286,25 @@ class WikiPage(HookMixin):
message = "Moved %s to %s" % (self.name, new_name)
os.rename(os.path.join(self.wiki.path, old_filename), os.path.join(self.wiki.path, new_filename))
self.wiki.gittle.add(new_filename)
self.wiki.gittle.rm(old_filename)
commit = self.wiki.commit(name=username,
email=email,
message=message,
files=[old_filename, new_filename])
self._clear_cache()
old_history = cache.get(self._cache_key('history'))
self._invalidate_cache()
self.name = new_name
self.filename = new_filename
# We need to clear the cache for the new name as well as the old
self._clear_cache()
self._invalidate_cache(save_history=old_history)
return commit
def write(self, content, message=None, create=False, username=None, email=None):
def write(self, content, message=None, username=None, email=None):
"""Write page to git repo
: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.
@ -283,9 +318,6 @@ class WikiPage(HookMixin):
with open(self.wiki.path + "/" + self.filename, 'w') as f:
f.write(content)
if create:
self.wiki.gittle.add(self.filename)
if not message:
message = "Updated %s" % self.name
@ -296,7 +328,8 @@ class WikiPage(HookMixin):
message=message,
files=[self.filename])
self._clear_cache()
old_history = cache.get(self._cache_key('history'))
self._invalidate_cache(save_history=old_history)
return ret
def revert(self, commit_sha, message, username, email):
@ -315,8 +348,7 @@ class WikiPage(HookMixin):
raise PageNotFound('Commit not found')
if not message:
commit_info = gittle.utils.git.commit_info(self.wiki.gittle[commit_sha.encode('latin-1')])
message = commit_info['message']
message = "Revert '%s' to %s" % (self.name, commit_sha[:7])
return self.write(new_page.data, message=message, username=username, email=email)

View file

@ -0,0 +1,430 @@
function Aced(settings) {
var id,
options,
editor,
element,
preview,
previewWrapper,
profile,
autoInterval,
themes,
themeSelect,
loadedThemes = {};
settings = settings || {};
options = {
sanitize: true,
preview: null,
editor: null,
theme: 'idle_fingers',
themePath: '/static/vendor/ace-builds/src',
mode: 'markdown',
autoSave: true,
autoSaveInterval: 5000,
syncPreview: false,
keyMaster: false,
submit: function(data){ alert(data); },
showButtonBar: false,
themeSelect: null,
submitBtn: null,
renderer: null,
info: null
};
themes = {
chrome: "Chrome",
clouds: "Clouds",
clouds_midnight: "Clouds Midnight",
cobalt: "Cobalt",
crimson_editor: "Crimson Editor",
dawn: "Dawn",
dreamweaver: "Dreamweaver",
eclipse: "Eclipse",
idle_fingers: "idleFingers",
kr_theme: "krTheme",
merbivore: "Merbivore",
merbivore_soft: "Merbivore Soft",
mono_industrial: "Mono Industrial",
monokai: "Monokai",
pastel_on_dark: "Pastel on Dark",
solarized_dark: "Solarized Dark",
solarized_light: "Solarized Light",
textmate: "TextMate",
tomorrow: "Tomorrow",
tomorrow_night: "Tomorrow Night",
tomorrow_night_blue: "Tomorrow Night Blue",
tomorrow_night_bright: "Tomorrow Night Bright",
tomorrow_night_eighties: "Tomorrow Night 80s",
twilight: "Twilight",
vibrant_ink: "Vibrant Ink"
};
function editorId() {
return "aced." + id;
}
function infoKey() {
return editorId() + ".info";
}
function gc() {
// Clean up localstorage
store.forEach(function(key, val) {
var re = new RegExp("aced\.(.*?)\.info");
var info = re.exec(key);
if (!info || !val.time) {
return;
}
var id = info[1];
// Remove week+ old stuff
var now = new Date().getTime() / 1000;
if (now > (val.time + 604800)) {
store.remove(key);
store.remove('aced.' + id);
}
});
}
function buildThemeSelect() {
var $sel = $("<select class='aced-theme-sel' data-placeholder='Theme'></select>");
$sel.append('<option></option>');
$.each(themes, function(k, v) {
$sel.append("<option value='" + k + "'>" + v + "</option>");
});
return $("<div/>").html($sel);
}
function toJquery(o) {
return (typeof o == 'string') ? $("#" + o) : $(o);
}
function initProfile() {
profile = {theme: ''};
try {
// Need to merge in any undefined/new properties from last release
// Meaning, if we add new features they may not have them in profile
profile = $.extend(true, profile, store.get('aced.profile'));
} catch (e) { }
}
function updateProfile(obj) {
profile = $.extend(null, profile, obj);
store.set('profile', profile);
}
function render(content) {
return (options.renderer) ? options.renderer(content) : content;
}
function bindKeyboard() {
// CMD+s TO SAVE DOC
key('command+s, ctrl+s', function (e) {
submit();
e.preventDefault();
});
var saveCommand = {
name: "save",
bindKey: {
mac: "Command-S",
win: "Ctrl-S"
},
exec: function () {
submit();
}
};
editor.commands.addCommand(saveCommand);
}
function info(info) {
if (info) {
store.set(infoKey(), info);
}
return store.get(infoKey());
}
function val(val) {
// Alias func
if (val) {
editor.getSession().setValue(val);
}
return editor.getSession().getValue();
}
function discardDraft() {
stopAutoSave();
store.remove(editorId());
store.remove(infoKey());
location.reload();
}
function save() {
store.set(editorId(), val());
}
function submit() {
store.remove(editorId());
store.remove(editorId() + ".info");
options.submit(val());
}
function autoSave() {
if (options.autoSave) {
autoInterval = setInterval(function () {
save();
}, options.autoSaveInterval);
} else {
stopAutoSave();
}
}
function stopAutoSave() {
if (autoInterval){
clearInterval(autoInterval)
}
}
function renderPreview() {
if (!preview) {
return;
}
preview.html(render(val()));
$('pre code', preview).each(function(i, e) {
hljs.highlightBlock(e)
});
}
function getScrollHeight($prevFrame) {
// Different browsers attach the scrollHeight of a document to different
// elements, so handle that here.
if ($prevFrame[0].scrollHeight !== undefined) {
return $prevFrame[0].scrollHeight;
} else if ($prevFrame.find('html')[0].scrollHeight !== undefined &&
$prevFrame.find('html')[0].scrollHeight !== 0) {
return $prevFrame.find('html')[0].scrollHeight;
} else {
return $prevFrame.find('body')[0].scrollHeight;
}
}
function getPreviewWrapper(obj) {
// Attempts to get the wrapper for preview based on overflow prop
if (!obj) {
return;
}
if (obj.css('overflow') == 'auto' || obj.css('overflow') == 'scroll') {
return obj;
} else {
return getPreviewWrapper(obj.parent());
}
}
function syncPreview() {
var editorScrollRange = (editor.getSession().getLength());
var previewScrollRange = (getScrollHeight(preview));
// Find how far along the editor is (0 means it is scrolled to the top, 1
// means it is at the bottom).
var scrollFactor = editor.getFirstVisibleRow() / editorScrollRange;
// Set the scroll position of the preview pane to match. jQuery will
// gracefully handle out-of-bounds values.
previewWrapper.scrollTop(scrollFactor * previewScrollRange);
}
function asyncLoad(filename, cb) {
(function (d, t) {
var leScript = d.createElement(t)
, scripts = d.getElementsByTagName(t)[0];
leScript.async = 1;
leScript.src = filename;
scripts.parentNode.insertBefore(leScript, scripts);
leScript.onload = function () {
cb && cb();
}
}(document, 'script'));
}
function setTheme(theme) {
var cb = function(theme) {
editor.setTheme('ace/theme/'+theme);
updateProfile({theme: theme});
};
if (loadedThemes[theme]) {
cb(theme);
} else {
asyncLoad(options.themePath + "/theme-" + theme + ".js", function () {
cb(theme);
loadedThemes[theme] = true;
});
}
}
function initSyncPreview() {
if (!preview || !options.syncPreview) return;
previewWrapper = getPreviewWrapper(preview);
window.onload = function () {
/**
* Bind synchronization of preview div to editor scroll and change
* of editor cursor position.
*/
editor.session.on('changeScrollTop', syncPreview);
editor.session.selection.on('changeCursor', syncPreview);
};
}
function initProps() {
// Id of editor
if (typeof settings == 'string') {
settings = { editor: settings };
}
if ('theme' in profile && profile['theme']) {
settings['theme'] = profile['theme'];
}
if (settings['preview'] && !settings.hasOwnProperty('syncPreview')) {
settings['syncPreview'] = true;
}
$.extend(options, settings);
if (options.editor) {
element = toJquery(options.editor);
}
$.each(options, function(k, v){
if (element.data(k.toLowerCase())) {
options[k] = element.data(k.toLowerCase());
}
});
if (options.themeSelect) {
themeSelect = toJquery(options.themeSelect);
}
if (options.submitBtn) {
var submitBtn = toJquery(options.submitBtn);
submitBtn.click(function(){
submit();
});
}
if (options.preview) {
preview = toJquery(options.preview);
// Enable sync unless set otherwise
if (!settings.hasOwnProperty('syncPreview')) {
options['syncPreview'] = true;
}
}
if (!element.attr('id')) {
// No id, make one!
id = Math.random().toString(36).substring(7);
element.attr('id', id);
} else {
id = element.attr('id')
}
}
function initEditor() {
editor = ace.edit(id);
setTheme(profile.theme || options.theme);
editor.getSession().setMode('ace/mode/' + options.mode);
if (store.get(editorId()) && store.get(editorId()) != val()) {
editor.getSession().setValue(store.get(editorId()));
}
editor.getSession().setUseWrapMode(true);
editor.getSession().setTabSize(2);
editor.getSession().setUseSoftTabs(true);
editor.setShowPrintMargin(false);
editor.renderer.setShowInvisibles(true);
editor.renderer.setShowGutter(false);
if (options.showButtonBar) {
var $btnBar = $('<div class="aced-button-bar aced-button-bar-top">' + buildThemeSelect().html() + ' <button type="button" class="btn btn-primary btn-xs aced-save">Save</button></div>')
element.find('.ace_content').before($btnBar);
$(".aced-save", $btnBar).click(function(){
submit();
});
if ($.fn.chosen) {
$('select', $btnBar).chosen().change(function(){
setTheme($(this).val());
});
}
}
if (options.keyMaster) {
bindKeyboard();
}
if (preview) {
editor.getSession().on('change', function (e) {
renderPreview();
});
renderPreview();
}
if (themeSelect) {
themeSelect
.find('li > a')
.bind('click', function (e) {
setTheme($(e.target).data('value'));
$(e.target).blur();
return false;
});
}
if (options.info) {
// If no info exists, save it to storage
if (!store.get(infoKey())) {
store.set(infoKey(), options.info);
} else {
// Check info in storage against one passed in
// for possible changes in data that may have occurred
var info = store.get(infoKey());
if (info['sha'] != options.info['sha'] && !info['ignore']) {
// Data has changed since start of draft
$(document).trigger('shaMismatch');
}
}
}
$(this).trigger('ready');
}
function init() {
gc();
initProfile();
initProps();
initEditor();
initSyncPreview();
autoSave();
}
init();
return {
editor: editor,
submit: submit,
val: val,
discard: discardDraft,
info: info
};
}

View file

@ -0,0 +1,52 @@
// Helper to get hash from end of URL or generate a random one.
function getExampleRef() {
var ref = new Firebase('https://' + Config['FIREBASE_HOSTNAME']);
var hash = window.location.hash.replace(/^#fp-/, '');
if (hash) {
ref = ref.child(hash);
} else {
ref = ref.push(); // generate unique location.
window.location = window.location + '#fp-' + ref.name(); // add it as a hash to the URL.
}
return ref;
}
function initFirepad() {
var new_ = true;
if (window.location.hash.lastIndexOf('#fp-', 0) === 0) {
new_ = false;
}
var firepadRef = getExampleRef();
var session = aced.editor.session;
var content;
if (new_) {
content = session.getValue();
}
// Firepad wants an empty editor
session.setValue('');
//// Create Firepad.
var firepad = Firepad.fromACE(firepadRef, aced.editor, {
defaultText: content
});
firepad.on('ready', function() {
startCollaboration();
});
$(document).on('end-collaboration', function() {
firepad.dispose();
});
}
$(document).on('loading-collaboration', function() {
initFirepad(true);
});
$(function(){
if (window.location.hash.lastIndexOf('#fp-', 0) === 0) {
loadingCollaboration();
}
});

View file

@ -0,0 +1,36 @@
var $startCollaborationBtn = $('#start-collaboration');
var $endCollaborationBtn = $('#end-collaboration');
var $loadingCollaborationBtn = $('#loading-collaboration');
function loadingCollaboration() {
$endCollaborationBtn.hide();
$startCollaborationBtn.hide();
$loadingCollaborationBtn.show();
$(document).trigger('loading-collaboration');
}
function startCollaboration() {
$loadingCollaborationBtn.hide();
$startCollaborationBtn.hide();
$endCollaborationBtn.show();
$(document).trigger('start-collaboration');
}
function endCollaboration() {
$loadingCollaborationBtn.hide();
$endCollaborationBtn.hide();
$startCollaborationBtn.show();
$(document).trigger('end-collaboration');
}
$(function() {
$startCollaborationBtn.click(function(e) {
loadingCollaboration();
e.preventDefault();
});
$endCollaborationBtn.click(function(e) {
endCollaboration();
e.preventDefault();
});
});

View file

@ -0,0 +1,28 @@
$(document).on('loading-collaboration', function() {
TogetherJS();
});
$(document).on('end-collaboration', function() {
TogetherJS();
});
TogetherJSConfig_toolName = "Collaboration";
TogetherJSConfig_suppressJoinConfirmation = true;
if (User.is_authenticated) {
TogetherJSConfig_getUserName = function () {
return User.username;
};
TogetherJSConfig_getUserAvatar = function () {
return User.avatar;
};
}
TogetherJSConfig_on_ready = function () {
startCollaboration();
};
TogetherJSConfig_on_close = function () {
//endCollaboration();
};

View file

@ -0,0 +1,119 @@
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(){
$entry_markdown.addClass('active');
$entry_preview.removeClass('active');
});
$entry_preview_header.click(function(){
$entry_preview.addClass('active');
$entry_markdown.removeClass('active');
});
$(document).on('shaMismatch', function() {
bootbox.dialog({
title: "Page has changed",
message: "This page has changed and differs from your draft. What do you want to do?",
buttons: {
ignore: {
label: "Ignore",
className: "btn-default",
callback: function() {
var info = aced.info();
info['ignore'] = true;
aced.info(info);
}
},
discard: {
label: "Discard Draft",
className: "btn-danger",
callback: function() {
aced.discard();
}
},
changes: {
label: "Show Diff",
className: "btn-primary",
callback: function() {
bootbox.alert("Draft diff not done! Sorry");
}
}
}
})
});
$(function(){
$("#discard-draft-btn").click(function() {
aced.discard();
});
$(".entry-markdown .floatingheader").click(function(){
aced.editor.focus();
});
$("#delete-page-btn").click(function() {
bootbox.confirm('Are you sure you want to delete this page?', function(result) {
if (result) {
deletePage();
}
});
});
});
var deletePage = function() {
var pageName = $page_name.val();
var path = Config['RELATIVE_PATH'] + '/' + pageName;
$.ajax({
type: 'DELETE',
url: path,
}).done(function(data) {
var msg = 'Deleted page: ' + pageName;
bootbox.alert(msg, function() {
location.href = '/';
});
}).fail(function(data, status, error) {
bootbox.alert('Error deleting page!');
});
};
var aced = new Aced({
editor: $('#entry-markdown-content').find('.editor').attr('id'),
renderer: function(md) { return MDR.convert(md) },
info: Commit.info,
submit: function(content) {
var data = {
name: $page_name.val().replace(/^\/*/g, "").replace(/\/+/g, "/"),
message: $page_message.val(),
content: content
};
// If renaming an existing page, use the old page name for the URL to PUT to
var subPath = (PAGE_NAME) ? PAGE_NAME : data['name'];
var path = Config['RELATIVE_PATH'] + '/' + subPath;
var newPath = 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) {
var res = data['responseJSON'];
if (res && res['error']) {
$page_name.addClass('parsley-error');
bootbox.alert("<h3>" + res['message'] + "</h3>");
} else {
location.href = newPath;
}
});
}
});

View file

@ -0,0 +1,16 @@
{% extends 'layout.html' %}
{% block body %}
<h2>History for <strong>{{ name }}</strong></h2>
<div class="pull-right">
<a href="{{ url_for('wiki.commit', name=name, sha=old) }}" class="btn btn-default btn-sm">View Old</a>
<a href="{{ url_for('wiki.commit', name=name, sha=new) }}" class="btn btn-info btn-sm">View New</a>
</div>
<p>
<a class="btn btn-default btn-sm" href="{{ url_for('wiki.history', name=name) }}">Back to History</a>
</p>
{{ diff|safe }}
<p>
<a class="btn btn-default btn-sm" href="{{ url_for('wiki.history', name=name) }}">Back to History</a>
</p>
{% endblock %}

View file

@ -0,0 +1,173 @@
{% extends 'layout.html' %}
{% block js %}
<script>
var Commit = {};
Commit.info = {{ info|tojson }};
var PAGE_NAME = '{{ name }}';
</script>
<script src="{{ url_for('wiki.static', filename='js/editor.js') }}"></script>
{% if partials %}
<script>
$(function() {
{% for name, value in partials.items() %}
{% if name and value %}
try {
Handlebars.registerPartial({{ name|tojson|safe }}, {{ value.data|tojson|safe }});
} catch (e) {
// no data?
}
{% endif %}
{% endfor %}
});
</script>
{% endif %}
{% if config.get('COLLABORATION') %}
<script src="{{ url_for('wiki.static', filename='js/collaboration/main.js') }}"></script>
{% endif %}
{% if config.get('COLLABORATION') == 'firepad' %}
<script>
Config['FIREBASE_HOSTNAME'] = {{ config.get('FIREBASE_HOSTNAME')|tojson }};
</script>
<script src="https://cdn.firebase.com/js/client/1.0.17/firebase.js"></script>
<script src="https://cdn.firebase.com/libs/firepad/1.0.0/firepad.min.js"></script>
<script src="{{ url_for('wiki.static', filename='js/collaboration/firepad.js') }}"></script>
{% endif %}
{% if config.get('COLLABORATION') == 'togetherjs' %}
<script src="{{ url_for('wiki.static', filename='js/collaboration/togetherjs.js') }}"></script>
<script src="https://togetherjs.com/togetherjs-min.js"></script>
{% endif %}
{% endblock %}
{% block body %}
<div id="app-wrap">
<div id="app-controls" class="row">
<div class="col-xs-4 col-md-3">
<input id="page-name" type="text" class="form-control input-sm" name="name"
placeholder="Name" value="{{- name -}}" />
</div>
<div class="col-xs-4 col-md-3">
<input id="page-message" type="text" class="form-control input-sm" name="page-message"
placeholder="Comment" value="" />
</div>
<div class="col-md-6 col-xs-4 text-right">
{% if config.get('COLLABORATION') %}
<div class="btn-group">
<button style='display:none' class="btn btn-danger btn-sm" id="end-collaboration">
<i class="fa fa-comments-o"></i>
<span class="hidden-xs">End Collaboration</span>
</button>
</div>
<div class="btn-group">
<button style='display:none' class="btn btn-default btn-sm" id="loading-collaboration" type="button">
<i class="fa fa-cog fa-spin"></i>
<span class="hidden-xs">Loading</span>
</button>
</div>
{% endif %}
<div class="dropdown btn-group">
<button class="btn btn-default btn-sm dropdown-toggle" type="button" id="editor-actions"
data-toggle="dropdown" title="Actions">
<i class="fa fa-cog"></i>
<span class="hidden-xs hidden-sm">Actions <i class="fa fa-caret-down"></i></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="editor-actions">
{% if config.get('COLLABORATION') %}
<li role="presentation">
<a role="menuitem" tabindex="-1" href="#" id="start-collaboration">Collaborate</a>
</li>
<li role="presentation" class="divider"></li>
{% endif %}
<li role="presentation">
<a role="menuitem" tabindex="-1" href="#" id="discard-draft-btn">Delete Draft</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="-1" href="#" id="delete-page-btn">Delete Page</a>
</li>
</ul>
</div>
<div class="dropdown btn-group">
<button id="theme-list-btn" type="button" class="dropdown-toggle btn btn-default btn-sm"
data-toggle="dropdown" title="Change Theme">
<i class="fa fa-paint-brush"></i>
<span class="hidden-xs hidden-sm">Theme <i class="fa fa-caret-down"></i></span>
</button>
<ul id="theme-list" class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="theme-list">
<li><a tabindex="-1" href="#" data-value="chrome" >Chrome</a></li>
<li><a tabindex="-1" href="#" data-value="clouds" >Clouds</a></li>
<li><a tabindex="-1" href="#" data-value="clouds_midnight" >Clouds Midnight</a></li>
<li><a tabindex="-1" href="#" data-value="cobalt" >Cobalt</a></li>
<li><a tabindex="-1" href="#" data-value="crimson_editor" >Crimson Editor</a></li>
<li><a tabindex="-1" href="#" data-value="dawn" class="selected">Dawn</a></li>
<li><a tabindex="-1" href="#" data-value="dreamweaver" >Dreamweaver</a></li>
<li><a tabindex="-1" href="#" data-value="eclipse" >Eclipse</a></li>
<li><a tabindex="-1" href="#" data-value="idle_fingers" >idleFingers</a></li>
<li><a tabindex="-1" href="#" data-value="kr_theme" >krTheme</a></li>
<li><a tabindex="-1" href="#" data-value="merbivore" >Merbivore</a></li>
<li><a tabindex="-1" href="#" data-value="merbivore_soft" >Merbivore Soft</a></li>
<li><a tabindex="-1" href="#" data-value="mono_industrial" >Mono Industrial</a></li>
<li><a tabindex="-1" href="#" data-value="monokai" >Monokai</a></li>
<li><a tabindex="-1" href="#" data-value="pastel_on_dark">Pastel on Dark</a></li>
<li><a tabindex="-1" href="#" data-value="solarized_dark" >Solarized Dark</a></li>
<li><a tabindex="-1" href="#" data-value="solarized_light" >Solarized Light</a></li>
<li><a tabindex="-1" href="#" data-value="textmate" >TextMate</a></li>
<li><a tabindex="-1" href="#" data-value="tomorrow" >Tomorrow</a></li>
<li><a tabindex="-1" href="#" data-value="tomorrow_night">Tomorrow Night</a></li>
<li><a tabindex="-1" href="#" data-value="tomorrow_night_blue" >Tomorrow Night Blue</a></li>
<li><a tabindex="-1" href="#" data-value="tomorrow_night_bright" >Tomorrow Night Bright</a></li>
<li><a tabindex="-1" href="#" data-value="tomorrow_night_eighties" >Tomorrow Night 80s</a></li>
<li><a tabindex="-1" href="#" data-value="twilight" >Twilight</a></li>
<li><a tabindex="-1" href="#" data-value="vibrant_ink" >Vibrant Ink</a></li>
</ul>
</div>
<div class="btn-group">
{% if name in config['WIKI_LOCKED_PAGES'] %}
<a class="btn btn-danger btn-sm">
<i class="fa fa-lock"></i>
<span class="hidden-xs">Locked</span>
</a>
{% else %}
<a id="submit-btn" class="btn btn-primary btn-sm">
<i class="fa fa-save"></i>
<span class="hidden-xs">Save</span>
</a>
{% endif %}
</div>
</div>
</div>
<section class="entry-markdown active">
<header class="floatingheader" id="entry-markdown-header">
<small>Markdown</small>
<a class="markdown-help" href=""><span class="hidden">What is Markdown?</span></a>
</header>
<section id="entry-markdown-content" class="entry-markdown-content">
<div id="editor-{{ name | b64encode }}" data-submitbtn='submit-btn' data-themeselect="theme-list" data-mode="markdown"
data-preview="preview" class="editor">{{ content }}</div>
</section>
</section>
<section class="entry-preview">
<header class="floatingheader" id="entry-preview-header">
<small>Preview</small>
</header>
<section class="entry-preview-content">
<div id="preview"></div>
</section>
</section>
</div>
{% endblock %}

View file

@ -0,0 +1,120 @@
{% extends 'layout.html' %}
{% block body %}
<h2>History for <strong>{{ name }}</strong></h2>
<p>
<a class="btn btn-default btn-sm compare-revisions">Compare Revisions</a>
</p>
<table class="table table-bordered revision-tbl dataTable DTTT_selectable">
<thead>
<tr>
<th>Name</th>
<th>Revision Message</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<tr><td colspan="3" style="text-align: center">Loading file history...</td></tr>
</tbody>
</table>
<p>
<a class="btn btn-default btn-sm compare-revisions">Compare Revisions</a>
</p>
{% endblock %}
{% block css %}
<style type="text/css">
table.dataTable td {
transition: background-color 0.5s linear, color 0.5s linear;
transition-delay: 0.1s;
}
table.dataTable tr.active td {
transition: background-color 0.1s linear, color 0.1s linear;
transition-delay: 0s
}
table.dataTable tbody tr:hover {
background-color: #d8d8d8 !important;
}
</style>
{% endblock %}
{% block js %}
<script>
$(document).ready(function() {
var selected = [];
var selected_pos = [];
$('.dataTable').dataTable({
serverSide: true,
ajax: {
url: '{{ url_for('.history_data', name=name) }}',
dataSrc: function (data) {
$('.dataTable').data('fully_loaded', data.fully_loaded);
return data.data
}
},
ordering: false,
bFilter: false,
columns: [
{
"data": null,
"render": function (data) {
return '<img src="' + data.gravatar + '?s=20" class="avatar" />&nbsp;&nbsp;' + data.author
}
},
{
"data": null,
"render": function (data) {
return '<a href="' + data.link + '" class="label label-primary">View</a>&nbsp;&nbsp;' + data.message
}
},
{ "data": "date" }
],
rowCallback: function( row, data, index ) {
index += $('.dataTable').DataTable().page.info().start;
$(row).data('index', index);
if ( $.inArray(data.DT_RowId, selected) !== -1 ) {
$(row).addClass('active');
}
},
infoCallback: function( settings, start, end, max, total, pre ) {
if (!$('.dataTable').data('fully_loaded')) {
total += "+"
}
return "Showing " + start +" to "+ end + " of " + total + " revisions.";
}
});
$('.dataTable tbody').on('click', 'tr', function () {
var id = this.id;
var selected_index = $.inArray(id, selected);
if ( selected_index === -1 ) {
selected.push( id );
selected_pos.push( $(this).data('index') );
if ( selected.length > 2) {
// Only 2 selected at once
var shifted = selected.shift();
selected_pos.shift();
$('#' + shifted).removeClass('active');
}
} else {
selected.splice( selected_index, 1 );
selected_pos.splice( selected_index, 1);
}
$(this).toggleClass('active');
});
$(".compare-revisions").click(function(){
if (selected.length != 2) return;
if (selected_pos[1] > selected_pos[0]) {
selected.reverse()
}
revs = selected.join("..");
location.href = "{{ config.RELATIVE_PATH }}/_compare/{{ name }}/" + revs;
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,43 @@
{% extends 'layout.html' %}
{% block js %}
<script>
$(document).ready(function() {
$('.data-table').dataTable({'aaSorting': [[0, "asc"], [1, "asc"]]});
});
</script>
{% endblock %}
{% block body %}
<h2>Index of <a href="{{ url_for('wiki.index') }}">/</a>
{%- set parts = path.split('/') -%}
{%- for dir in parts if dir -%}
<a href="{{ url_for('wiki.index', path='/'.join(parts[:loop.index])) }}">{{ dir }}/</a>
{%- endfor -%}
</h2>
<table class="table table-bordered data-table">
<thead>
<tr>
<th style="width: 1px;"></th>
<th>Name</th>
<th class="hidden-xs">Bytes</th>
<th class="hidden-xs hidden-sm">Created</th>
<th>Modified</th>
</tr>
</thead>
{% for file in index %}
<tr>
{% if file['dir'] %}
<td><i class="fa fa-folder-open-o"><span style="display:none;">Dir</span></i></td>
<td><a href="{{ url_for('wiki.index', path=file['name']) }}">{{ file['name'][path|length:] }}</a></td>
{% else %}
<td><i class="fa fa-file-text-o"><span style="display:none;">Page</span></i></td>
<td><a href="{{ url_for('wiki.page', name=file['name']) }}">{{ file['name'][path|length:] }}</a></td>
{% endif %}
<td class="hidden-xs">{{ file['size'] }}</td>
<td>{{ file['ctime']|datetime }}</td>
<td class="hidden-xs hidden-sm">{{ file['mtime']|datetime }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -0,0 +1,34 @@
{% extends 'layout.html' %}
{% block page_menu %}
<div class="controls pull-right">
<a class="btn btn-default btn-sm" href="{{ url_for('wiki.edit', name=name) }}">Edit</a>
<a class="btn btn-default btn-sm" href="{{ url_for('wiki.history', name=name) }}">History</a>
</div>
{% endblock %}
{% block body %}
{% if commit %}
<div id="page-action-bar">
<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" />
</form>
</div>
{% endif %}
<div id="page-content"></div>
{% endblock %}
{% block js %}
<script>
$(function(){
{% if partials %}
{% for name, value in partials.items() %}
Handlebars.registerPartial({{ name|tojson|safe }}, {{ value.data|tojson|safe }});
{% endfor %}
{% endif %}
$("#page-content").html(MDR.convert({{ page.data|tojson|safe }})).show();
});
</script>
{% endblock %}

View file

@ -41,7 +41,7 @@ class WikiTest(WikiBaseTest):
self.assert_200(rv)
self.assert_context('name', 'test')
eq_(self.get_context_variable('page').info['message'], 'test message')
eq_(next(self.get_context_variable('page').history)['message'], 'test message')
eq_(self.get_context_variable('page').data, 'testing')
def test_history(self):

View file

@ -1,16 +1,18 @@
import itertools
import sys
from datetime import datetime
from flask import abort, g, render_template, request, redirect, Blueprint, flash, url_for, current_app
from flask.ext.login import login_required, current_user
from flask_login import login_required, current_user
from realms.lib.util import to_canonical, remove_ext, gravatar_url
from .models import PageNotFound
blueprint = Blueprint('wiki', __name__)
blueprint = Blueprint('wiki', __name__, template_folder='templates',
static_folder='static', static_url_path='/static/wiki')
@blueprint.route("/_commit/<sha>/<path:name>")
def commit(name, sha):
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
return current_app.login_manager.unauthorized()
cname = to_canonical(name)
@ -25,7 +27,7 @@ def commit(name, sha):
@blueprint.route(r"/_compare/<path:name>/<regex('\w+'):fsha><regex('\.{2,3}'):dots><regex('\w+'):lsha>")
def compare(name, fsha, dots, lsha):
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
return current_app.login_manager.unauthorized()
diff = g.current_wiki.get_page(name, sha=lsha).compare(fsha)
@ -40,7 +42,7 @@ def revert():
commit = request.form.get('commit')
message = request.form.get('message', "Reverting %s" % cname)
if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous():
if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous:
return dict(error=True, message="Anonymous posting not allowed"), 403
if cname in current_app.config.get('WIKI_LOCKED_PAGES'):
@ -62,13 +64,39 @@ def revert():
@blueprint.route("/_history/<path:name>")
def history(name):
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
return current_app.login_manager.unauthorized()
return render_template('wiki/history.html', name=name)
@blueprint.route("/_history_data/<path:name>")
def history_data(name):
"""Ajax provider for paginated history data."""
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
return current_app.login_manager.unauthorized()
hist = g.current_wiki.get_page(name).get_history()
for item in hist:
draw = int(request.args.get('draw', 0))
start = int(request.args.get('start', 0))
length = int(request.args.get('length', 10))
page = g.current_wiki.get_page(name)
items = list(itertools.islice(page.history, start, start + length))
for item in items:
item['gravatar'] = gravatar_url(item['author_email'])
return render_template('wiki/history.html', name=name, history=hist)
item['DT_RowId'] = item['sha']
date = datetime.fromtimestamp(item['time'])
item['date'] = date.strftime(current_app.config.get('DATETIME_FORMAT', '%b %d, %Y %I:%M %p'))
item['link'] = url_for('.commit', name=name, sha=item['sha'])
total_records, hist_complete = page.history_cache
if not hist_complete:
# Force datatables to fetch more data when it gets to the end
total_records += 1
return {
'draw': draw,
'recordsTotal': total_records,
'recordsFiltered': total_records,
'data': items,
'fully_loaded': hist_complete
}
@blueprint.route("/_edit/<path:name>")
@ -85,7 +113,8 @@ def edit(name):
return render_template('wiki/edit.html',
name=cname,
content=page.data,
info=page.info,
# TODO: Remove this? See #148
info=next(page.history),
sha=page.sha,
partials=page.partials)
@ -137,7 +166,7 @@ def _tree_index(items, path=""):
@blueprint.route("/_index", defaults={"path": ""})
@blueprint.route("/_index/<path:path>")
def index(path):
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
return current_app.login_manager.unauthorized()
items = g.current_wiki.get_index()
@ -158,7 +187,7 @@ def page_write(name):
if not cname:
return dict(error=True, message="Invalid name")
if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous():
if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous:
return dict(error=True, message="Anonymous posting not allowed"), 403
if request.method == 'POST':
@ -168,7 +197,6 @@ def page_write(name):
sha = g.current_wiki.get_page(cname).write(request.form['content'],
message=request.form['message'],
create=True,
username=current_user.username,
email=current_user.email)
@ -202,7 +230,7 @@ def page_write(name):
@blueprint.route("/", defaults={'name': 'home'})
@blueprint.route("/<path:name>")
def page(name):
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
return current_app.login_manager.unauthorized()
cname = to_canonical(name)